x-one/media-object-bundle
Symfony bundle for managing media objects in your application: attach files and images to entities, organize uploads, and handle storage through a flexible media object layer. Simplifies reusable media management and Doctrine integration.
x-one/media-object-bundle is a Symfony bundle whose entire job is to ship a known-good VichUploaderBundle configuration: a MediaObject-style entity, a Doctrine mapping, a storage/namer setup, and the wiring so you can attach files to your own entities without re-deriving the YAML every project. Treat it as a starter configuration, not a framework — you get a working upload path, not a media library.
Minimal steps:
composer require x-one/media-object-bundle will fail. Add a VCS entry to your root composer.json, then require it:
"repositories": [
{ "type": "vcs", "url": "https://bitbucket.org/majchw/media-object-bundle/" }
]
Then composer require x-one/media-object-bundle:dev-master (pin the commit hash you tested — see Gotchas).config/bundles.php. Also confirm VichUploaderBundle is registered — this bundle depends on it but may not pull it in transitively.bin/console doctrine:migrations:diff and inspect the generated migration before executing it. The entity's table name and columns are decided by the bundle, so read the diff.config/packages/vich_uploader.yaml, or wherever the bundle expects its config — check DependencyInjection/Configuration.php in the repo for the exact tree).MediaObject, then render its URL in Twig with vich_uploader_asset(mediaObject, 'file'). If that renders and the file appears under your web-accessible directory, the bundle did its job.Where to look first, in order:
README / Resources/doc/ — may be the only documentation.DependencyInjection/Configuration.php — the authoritative list of config keys and defaults.Resources/config/*.yaml (or services.php) — shows the mapping name, the entity class, and which services are public/aliased.Entity/ or Model/ — the MediaObject class, its file property, and the Vich mapping annotations/attributes.composer.json — required Symfony and Vich versions; this tells you immediately whether it will even install on your stack.Attach media to your domain entity. The intended pattern is a Doctrine relation to the bundle's media entity (or to a mapped superclass you extend):
#[ORM\ManyToOne(targetEntity: MediaObject::class, cascade: ['persist', 'remove'])]
#[ORM\JoinColumn(nullable: true)]
private ?MediaObject $image = null;
Cascade persist so the file row is written with your entity, and remove so deleting the owner cleans up the row. Whether the physical file is deleted depends on Vich's Doctrine listener being active for the mapping — verify it, don't assume.
Controller + form. The everyday flow is a standard Symfony form with a FileType bound to the media object's file property, then $form->handleRequest() → $em->persist() → $em->flush(). The interesting part happens in Vich's prePersist/preUpdate listeners, so your controller stays boring — that's the whole value proposition.
Rendering. Use vich_uploader_asset(media, 'file') rather than hand-building the path from uri_prefix. Hand-built paths are the #1 source of broken images when the prefix or a LiipImagine filter changes. If you need thumbnails, layer LiipImagineBundle on top and point the filter's loader at the same Vich mapping.
Collections of media. If your bundle's entity supports a mapping discriminator, use it for multiple collections (avatars vs. documents) so each gets its own directory and namer. If the configuration is hard-coded to a single mapping name, you're better off overriding the Vich config in your app rather than fighting the bundle.
Storage abstraction. For local dev this bundle almost certainly targets the local filesystem. For S3/GCS, swap Vich's storage service for the Flysystem implementation in your own config; keep the change at the config layer so the entity and controllers don't care.
Testing. Point the mapping at a temporary directory in config/packages/test/. Create uploads with new UploadedFile($path, 'name.jpg', 'image/jpeg', null, true) (test mode = true). Assert on the persisted row and on file_exists() of the stored path; don't assert on the exact generated filename unless you've pinned the namer.
Keep an exit hatch. Because the package is config-only, wrap access behind your own service or repository (e.g. MediaManager::attach()), and type-hint against your domain rather than importing MediaObject everywhere. If the bundle is abandoned, you inline ~40 lines of Vich YAML and delete your wrapper implementation.
"x-one/media-object-bundle": "dev-master#abc1234") so a force-push doesn't silently change your upload behavior. Vendoring the config into config/packages/vich_uploader.yaml and dropping the dependency is a legitimate first-week decision.uri_prefix vs. upload_destination mismatch produces images that render a 200-looking path but 404. Dump the resolved config (bin/console config:dump-reference vich_uploader for the schema, and inspect your container parameters) and cross-check against where files actually landed on disk.inject_on_load. Without it, re-saving an entity that already has a file can blank the property, because the form's file field is empty on edit. Enable it so Vich re-hydrates the existing file object during load.File constraint (max size + mimeTypes), since clients lie about extensions.Mapping not found for field "file" and "file could not be found for field". Both mean your entity's Vich annotation and the bundle's configured mapping name disagree. Use bin/console debug:container to find the storage/namer service IDs actually registered.AppKernel-era service definitions, and any use of removed UploadedFile APIs or getClientOriginalName() sanitization assumptions.load(), your overrides may silently lose. Read DependencyInjection/*Extension.php before spending an hour debugging why your uri_prefix change did nothing.MediaObject rather than a MappedSuperclass, subclassing it with Doctrine inheritance is painful. Prefer composition (a relation) over inheritance unless the repo explicitly documents the superclass approach.uri_prefix are world-readable by URL. For invoices, ID scans, or anything tenant-scoped, store outside the web root and stream through a controller with a voter check — don't rely on unguessable filenames.How can I help you explore Laravel packages today?