misaf/vendra-multimedia-api
A Laravel/PHP package that wraps the Vendra multimedia API in a simple integration layer, helping developers fetch and manage multimedia resources with less boilerplate. It provides a convenient way to add Vendra multimedia support to Laravel applications.
Understand what you're actually installing. Despite the description's talk of "uploading" and "custom storage and response formats," the package as documented is the read-only API Platform layer for misaf/vendra-multimedia. It exposes Multimedia domain models as JSON:API-ish HTTP resources. Uploads, transformations, and storage live in the sibling domain package misaf/vendra-multimedia — not here. Plan your day-to-day work accordingly: this is a consumption surface, not an ingestion pipeline.
Install. This is a subtree split of the Vendra monorepo, published read-only. You do not PR here; changes go to misaf/vendra and get split out. Pin explicitly:
composer require misaf/vendra-multimedia-api:^1.12.3
It is designed to run inside a Vendra host application, not as a standalone Laravel package. The module's service provider registers src/ApiResource into api-platform.resources and tags its state providers as ProviderInterface; API Platform generates the routes from the resource operations. There are no route files to publish — and you should not add any.
Where to look first, in order:
composer.json — the real dependency contract, including which Vendra platform packages are transitively exposed.src/ApiResource/MultimediaResource.php — the public contract. A final readonly DTO annotated with #[ApiResource], declaring Get/GetCollection with explicit uriTemplate paths and a provider.src/State/ — the links handler (e.g. MultimediaLinksHandler) and the mapper (e.g. MultimediaMapper). All Eloquent querying, hydration, filtering, and pagination happens here.ServiceProvider — bootstrapping is deliberately minimal; read it once and you know the whole wiring.tests/ including tests/ArchTest.php — the architecture rules are encoded as tests, which is the fastest way to learn what you must not do.First use case. Hit GET /content/multimedia with pagination and a filter, then GET /content/multimedia/{id}. Confirm the shortName and URI path (/content/multimedia, kebab-case) against the DTO before you write any client code — these are the contract, and they are stable only because the tests enforce it.
Consuming it from a front end or mobile client. The DTO is the contract. Treat it as such:
title, description) come back as array<string, string> locale maps, not scalars. Your client-side deserializer must handle a map, and your UI picks a locale or falls back.ResourceReference rather than expecting an embedded foreign model. Related resources are referenced, not inlined.Adding a query parameter. This is the most common day-to-day change and it has a fixed three-part shape:
QueryParameter with an API Platform filter (EqualsFilter, BooleanFilter, …) and Laravel constraints.property to the model column the state provider will filter on.Doing 1 and 2 without 3 produces a parameter that is documented but silently ignored — the classic failure mode here.
Translated-column filters. Filter on the active-locale JSON path, not the bare column. WHERE title = ? against a JSON column will not do what you want. The provider reads Laravel's current locale, so the host app can resolve locale however it likes — the module stays localization-package agnostic, and you should keep it that way.
Adding an exposed field. Touch three places: the DTO property (with an explicit type), the mapper that serializes it, and a test asserting the shape. If the field is translatable, it must be in the model's $translatable array and backed by a JSON column — and then the DTO type is array<string, string>. If it is not in $translatable, it must be a scalar column with a scalar DTO type. Do not half-translate a field.
Adding a mutation. Resist it. Resources are read-only unless a write is an explicit product decision. If you genuinely need one, that decision belongs in the domain package with the persistence rules, and the API module only surfaces it.
Testing workflow. From the host app:
php artisan test --compact --testsuite=vendra-multimedia-api
composer stan
vendor/bin/pint --dirty --format agent # only if PHP files changed
Add tests for every new operation, query parameter, and any change to resource shape, shortName, or URI path. Keep the Pest arch presets in tests/ArchTest.php intact, including the expectation that the module does not use Misaf\VendraTenant. If you're changing Blade markup, the Tailwind skill applies; otherwise leave front-end concerns out of this package entirely.
Isolating yourself as a consumer. If your app depends on this API, put your own client/gateway interface in front of it and write contract tests against your interface, not against the package's internals. Single-maintainer namespace, thin release notes, no SLA — the adapter boundary is what keeps a future swap or a direct-HTTP fallback a container rebinding rather than a refactor.
tenant_id must be in the selected columns for public media URLs. This is the single most likely source of a subtle production bug: URLs render for some records and come back broken or unscoped for others, and it only reproduces once real multi-tenant data exists. If you change the provider's select, you can break URL generation without breaking the response shape. Test it with a tenant-scoped fixture.
Do not add tenancy to this module. Production API code must be tenant-provider agnostic — inherit tenancy from the domain models. No API tenant toggle, no Misaf\VendraTenant references in resources, state providers, or query parameters. Feature tests may use a concrete tenant factory purely to establish context; that exception is for tests only, and ArchTest.php enforces it.
Keep shortName and URI paths stable. Renaming content/multimedia or its shortName is a breaking change for every consumer, even though the package version won't say so. Add a test whenever they change, and treat any such change as a coordinated release.
Filters can be documented and inert. See above — declaration and application are separate steps in separate files. When a filter "doesn't work," check the state provider before you check the annotation.
Translatable vs. scalar is a domain decision, not a convenience. A field in $translatable must be JSON-columned end to end: model traits/casts, factories, validation, Filament locale UI (in the domain package), API serialization, and tests. A field not in $translatable must be scalar and must not pick up Spatie Translatable, translatable slug traits, locale switchers, or translation-shaped array data. Mixed state here produces data that serializes fine and queries wrong.
The description over-promises. "Uploading," "custom storage," and "extensible response formats" are not what the code does. Scope estimates off the source and the skill doc, not the README blurb. If you were brought in expecting an upload endpoint, that work lives in the domain package or doesn't exist yet.
Respect the Vendra transitive API policy. A Vendra dependency intentionally exposed through the public API of a directly required platform package is part of that package's supported contract — do not add a redundant direct composer.json requirement just because you import a type from it. This applies only to Vendra platform packages under require, never to require-dev, suggest, incidental implementation deps, or third-party packages. And keep self.version aligned across the Vendra package graph; skew between sibling modules is a nasty, non-obvious class of failure.
Don't hand-register routes for resource endpoints. Routes come from API Platform resource operations. A manually registered route will shadow or duplicate the generated one and the failure looks like a routing bug rather than a config bug.
No Filament or admin UI in this module. Keep API resource DTOs, state providers, query parameters, service providers, and API tests here; import domain models from Misaf\VendraMultimedia rather than duplicating them. When in doubt about whether something belongs, the arch tests are the arbiter.
Release notes carry no information. v1.12.3 publishes a bare auto-generated compare link. Before upgrading across minors (v1.10.0 → v1.12.3 is two of them), read the diff yourself and specifically check for signature changes on public methods, config key renames, and exception type changes — the three categories that break a wrapper even within semver. Schedule it as routine maintenance, but do not merge on the assumption that "minor means additive."
Debugging checklist, in order: is the parameter applied in the state provider? is tenant_id selected? is the filter hitting the active-locale JSON path? is the field actually in $translatable and JSON-columned? is the URL relative and being resolved client-side? Ninety percent of "the API is wrong" reports land in one of those five.
How can I help you explore Laravel packages today?