knplabs/knp-media-exposer-bundle
Symfony bundle that turns media references into public or protected URLs. Define media paths, apply voters to control access, and generate signed links for images, videos, or downloads. Custom resolvers and URL generators keep file exposure logic out of your controllers.
Reality check first: this is a Symfony bundle, not a Laravel package. It wires the knplabs/media-exposer library into Symfony's DI container; there is no service provider, facade, or config file that transfers to a Laravel app, and both the bundle and its underlying library are archived. On a modern Laravel 10/11 stack composer require knplabs/knp-media-exposer-bundle will almost certainly fail on PHP/Symfony platform constraints. The only realistic scenario where you "get started" day-to-day is inheriting a legacy Symfony 2.x/3.x application where the bundle is already wired and you need to keep existing media URL calls working or port them off.
If you are on that legacy stack, the minimal wiring is roughly:
composer require knplabs/knp-media-exposer-bundle
// app/AppKernel.php
$bundles = [
// ...
new Knp\Bundle\MediaExposerBundle\KnpMediaExposerBundle(),
];
Then point the bundle at a media source in app/config/config.yml (a local directory or a remote/HTTP source), and inject the exposer service into a controller to resolve an identifier to a URL:
$url = $this->get('knp_media_exposer.exposer')
->getUrl($mediaId, ['source' => 'default']);
Where to look first: read KnpMediaExposerBundle.php and its DependencyInjection/*Extension and Resources/config/services.xml. The whole bundle is small — you can read it end to end in an hour — and the exact service IDs, tags, and config keys you actually need are defined there. Verify the service ID before copying any snippet, because the internal names shifted across the 2.x line. The first useful use case is almost always: "a template or controller needs a stable public URL for a media asset whose storage backend we don't want to hard-code."
For a Laravel target, the correct "getting started" is to stop and use what Laravel already ships: Storage::disk()->url() and Storage::disk()->temporaryUrl() cover the same identifier-to-URL capability with zero lifecycle risk.
The bundle's day-to-day value, wherever it is used at all, is the pattern of hiding storage layout behind an identifier → URL call. The patterns you'd actually write against it:
$this->get(...) when you touch the legacy code — it makes the swap-out path for a future replacement far cheaper.source option at call time. This is the closest the package gets to "environment-aware URL generation," and it is the piece you would most want to reproduce in a rewrite.In a Laravel reimplementation, each of these maps onto a thin, testable replacement:
// app/Services/MediaUrlResolver.php (illustrative replacement)
class MediaUrlResolver
{
public function url(string $mediaId, ?int $ttl = null): string
{
$disk = $this->strategyFor($mediaId); // config map, not a Symfony tag
return $ttl
? Storage::disk($disk)->temporaryUrl($mediaId, now()->addSeconds($ttl))
: Storage::disk($disk)->url($mediaId);
}
}
Strategy registry = config/filesystems.php; adapter tagging = a config array; the "exposer service" = a single injected resolver. That is a day or two of work and you own it.
composer.json before any design discussion. A throwaway composer require in a scratch branch is the cheapest possible test and usually ends the conversation.knplabs/media-exposer is yours to fix by forking. Budget SCA scanning as a standing cost if it touches production.knp_media_exposer.exposer and the adapter tags are internal contract from an old Symfony era. Confirm them in Resources/config/services.xml/the DI extension — guessing from blog posts is how you get a "service not found" hours later. When debugging, dump the compiled container (app/console container:debug) and grep for media_exposer.cache:clear and read the extension class — the valid keys are in Configuration/load().name attribute) simply won't appear in the strategy map — and the failure mode is usually "default source used silently" rather than an exception. Assert on the resolved source, not just the URL, in any test you write around this.Storage + a thin resolver covers the requirement, and the real extension point (your own strategy map) is where your requirements differ anyway.How can I help you explore Laravel packages today?