thrace-project/media-bundle
Symfony bundle for managing media files and images: upload handling, storage and entity integration, reusable media entities, and helpers for attaching files to Doctrine-backed content. A practical foundation for media libraries and galleries.
This is a Symfony bundle, not a Laravel package. Before anything else, confirm that your project is a Symfony full-stack app (or that you are willing to run one alongside Laravel). Laravel has no bundle/kernel/DI-extension system, so ThraceMediaBundle cannot be composer require'd into a Laravel app and "just work" — see Gotchas for what to use instead if you are Laravel-only.
For a Symfony app, the shortest path:
composer require thrace-project/media-bundle
Then read, in this order:
composer.json — check the symfony/*, php, doctrine/* and oneup/uploader-bundle constraints. This package predates Symfony 4 flex and will almost certainly pin Symfony 2.x/3.x and PHP 5.6/7.x. If your app is Symfony 5+, stop here.DependencyInjection/Configuration.php — the actual config surface (media_class, upload directory, image filter definitions, entity mapping strategy).Entity/Media.php + MediaManager — the whole package is essentially "a Doctrine entity for a file row plus a service that writes bytes to disk and populates the row."Resources/views/ and Controller/ — the bundled media browser/upload endpoints you are expected to embed.Register and configure:
// app/AppKernel.php
new Thrace\MediaBundle\ThraceMediaBundle(),
new Oneup\UploaderBundle\OneupUploaderBundle(), // upload transport this bundle leans on
# app/config/config.yml
thrace_media:
media_class: AppBundle\Entity\Media # your subclass of the bundle entity
# upload_dir / file_system / image filters per Configuration.php
Map your entity (the bundle ships a mapped superclass or an entity you subclass — check which your version uses), then:
php app/console doctrine:schema:update --force
First use case to prove it out: attach one image to one entity, render it. Add a MediaType field to an existing form, upload a JPEG, confirm the row in media and the file on disk, and render the path in Twig. If that loop works end-to-end, the rest of the bundle is variations on it; if it doesn't, you've found the version-incompatibility problem early and cheaply.
1. Form-driven single/multiple media attach. The primary integration is a form field, not a controller.
$builder->add('image', MediaType::class, [
'type' => 'image', // file | image | video | audio
'multiple' => false,
]);
Let the bundle's form type render its own widget (it wires up the JS uploader for you). Don't hand-roll a <input type="file"> next to it — you'll get two competing upload paths and one will silently produce orphan rows.
2. MediaManager as the only write path. Never persist a Media row yourself. Always go through the manager so file naming, path resolution and metadata (contentType, size, type) stay consistent:
$media = $this->get('thrace_media.media_manager')->createMedia($uploadedFile, 'image');
$entity->setImage($media);
$em->persist($entity);
$em->flush();
Centralizing this makes the eventual "we need S3 / a CDN / virus scanning" change a one-class change instead of a grep across controllers.
3. CMS-style media browser for existing assets. The bundle's real value over a plain file input is reuse: browse previously uploaded media and pick one. Embed the bundled controller via a route/modal in your admin, and have your form persist the chosen Media id rather than re-uploading. This is the pattern for a back office where editors upload once and reuse across pages.
4. Image manipulations at upload time, not render time. Configure resize/crop/rotate variants in bundle config and let the manager generate them on save. Generating thumbnails on every request is the classic way to make this bundle look slow; pre-generating also means your templates stay dumb (<img src="{{ media.path }}"> plus a variant path).
5. Wrap it. Put a thin MediaService of your own in front of the bundle's services:
class MediaService
{
public function __construct(private MediaManager $manager) {}
public function attach(UploadedFile $file, string $type): Media { /* ... */ }
public function delete(Media $media): void { /* remove file + row */ }
}
You get one seam for testing, one place to add S3/orphan cleanup, and a fighting chance of swapping the package out later without touching controllers.
6. Lifecycle hooks for cleanup. Wire preRemove/postRemove on the owning entity to delete the underlying file. The bundle stores a path string; Doctrine will happily delete the row and leave the file behind forever.
Framework lock-in is the headline. This is a Symfony bundle. If you are a Laravel developer, the realistic assessment is: read it for ideas (media entity + manager + upload transport is the same shape as spatie/laravel-medialibrary), then use spatie/laravel-medialibrary for storage/associations and intervention/image for manipulations. Porting this bundle means re-implementing it. Do not budget for "adapting" it.
Maintenance signals are weak. 11 stars, no meaningful community, no security policy or recent activity. Treat the composer constraints as frozen: it targets an older Symfony/PHP generation, so PHP 8.1+ typed properties, Symfony 6 autowiring, and modern Doctrine will likely conflict. Pin exact versions and expect to vendor-patch. If you adopt it, do so with the assumption that you now maintain it.
Doctrine mapping surprises. Whether you subclass a mapped superclass or a concrete entity changes your migrations significantly. Decide before writing migrations; switching later means a schema rename dance. Also verify the media_class config points at your class, not the bundle's — a mismatch produces rows that don't hydrate.
Uploads land in a public directory with real filenames. There is no signed-URL or private-delivery story here. Validate MIME type and extension server-side (never trust the client), and be aware that "public path + guessable name" is a data-exposure pattern for anything user-submitted. If uploads are user-generated, put a reverse-proxy rule or a controller in front of the directory.
The upload endpoint is an HTTP surface you own. The file transport goes through a controller/route (historically OneupUploaderBundle + a JS uploader). Check that it is behind your firewall, that CSRF/session handling matches your app, and that it returns exactly the JSON shape the front-end JS expects. Most "upload does nothing" bugs are a 403 from the firewall or a response-format mismatch, not a bundle bug.
Orphaned files and orphaned rows. Two failure modes, both common: upload succeeds but the form never persists the entity (file on disk, no row); and entity deleted but file remains. Add a console command or cron that reconciles disk vs. DB — you will need it.
Debugging checklist, in order: (1) is the upload directory writable by the web user; (2) does thrace_media config actually load (dump the container, look for the media_manager service); (3) does the route exist (debug:router | grep media); (4) does the JS get a 200 JSON response; (5) did the row persist with a non-null path. That sequence resolves the large majority of integration failures.
Extension points worth knowing. Templates and translations can be overridden the Symfony way (app/Resources/ThraceMediaBundle/...), which is how you avoid forking just to change markup. The MediaManager is the intended service-level extension point for storage backends and manipulation logic — subclass or decorate it rather than editing vendor code.
Verdict for day-to-day use: viable only inside a legacy Symfony 2/3 application that already depends on it. For new work — Laravel or modern Symfony — the patterns are worth copying and the package itself is not worth adopting.
How can I help you explore Laravel packages today?