xrow/download-library-bundle
A PHP bundle for building and managing a download library. Organize downloadable files, expose them through your application, and integrate reusable library management into your existing framework.
First, a reality check: despite the "bundle" naming and the Laravel framing of this assessment, xrow/download-library-bundle is a Symfony bundle built for the eZ Publish / eZ Platform / Ibexa ecosystem (xrow GmbH ships eZ/Ibexa extensions). It will not drop into a Laravel app. If you are on Laravel, treat this as reference material for how to build a "download all subtree images as ZIP" feature yourself; if you are on Ibexa/eZ, this is directly usable.
Minimal steps on Ibexa:
composer require xrow/download-library-bundleXrow\DownloadLibraryBundle\XrowDownloadLibraryBundle::class => ['all' => true] to config/bundles.php.ezpublish/EzPublishKernel.php / settings/override/site.ini.append.php activation block.bin/console cache:clear (or php ezpublish/console cache:clear on legacy).bin/console debug:router | grep -i download and bin/console debug:container | grep -i xrow. The bundle's whole job is a controller action that walks a subtree location, collects image fields, streams a ZIP.Where to look first, in order:
Resources/config/routing.yml (or routing.yaml) — the actual URL you hit.Controller/ — this tells you exactly how the location ID is passed and how the ZIP is built.DependencyInjection/ — parameters and service IDs you can override.README.md — often thin on this package; the routing file is more truthful.First use case: expose a "Download all images" button on a gallery/album content view. Pass the current locationId, hit the bundle's route, receive a application/zip response. Confirm it works by hitting the route directly with a known location ID in the browser before wiring up any template.
Direct route wiring (fastest win). Fetch the route name via debug:router, then render a link from a Twig template:
<a href="{{ path('xrow_download_library_download', {'locationId': location.id}) }}">
Download all images
</a>
Guard the link with {{ ez_is_granted('read', location) }} (or the equivalent Ibexa policy check) so users only see the button for subtrees they can read. The bundle likely does not enforce this for you.
Service-level integration (recommended for anything non-trivial). Rather than depending on the bundle's controller, inject its ZIP-building service (find the ID via debug:container) into your own controller. This buys you:
Async pattern for large subtrees. A subtree with hundreds of images will blow past PHP's max_execution_time and memory_limit if the bundle buffers the ZIP in memory. For large libraries, wrap the ZIP generation in a Symfony Messenger / eZ Platform queue job:
BuildDownloadZipMessage($locationId, $userId).This is the pattern that holds up in production; the synchronous route is fine for demos and small albums.
Content-view template hook. Register a content view override so the button appears automatically on your gallery content type without touching every template:
# config/packages/views.yaml
ibexa:
system:
default:
content_view:
full:
gallery:
template: '@@Acme/gallery/full.html.twig'
match:
Identifier\ContentType: gallery
Filename and content-disposition hygiene. If you wrap or reimplement the bundle, sanitize the user-facing filename (slug the location name, strip quotes and semicolons) and set Content-Disposition: attachment; filename="..." explicitly. Do not trust the location name.
Testing seam. If you need to assert behavior, target the controller via a functional test hitting the route with a fixture location, and assert the response is application/zip and that the archive opens and contains the expected number of entries. Unit-testing the ZIP internals is usually not worth it; the route is the contract.
The package is effectively unmaintained. Zero stars and zero dependents on a public repo is a real signal. Before adopting: read the last commit date, check it against your Symfony/Ibexa version, and be prepared to fork. Pin the exact version in composer.json and vendor the fork if you need to patch.
Version skew with eZ/Ibexa. Bundles from this era frequently reference deprecated APIs (ezpublish kernel hooks, eZ\Publish\Core\… classes that moved in Ibexa 4). If the bundle won't boot, the first thing to diff is the composer.json require constraints against your platform version — not the code.
Memory and streaming. Two distinct failure modes:
output_buffering or an upstream proxy buffers the response, so users see a stall and then a timeout.Mitigations: raise memory_limit and max_execution_time for the route only (via a listener), disable output buffering (ob_end_clean() before streaming), and ensure ZipArchive (php-zip) is installed — it's a common missing extension that fails silently-ish.
Image variation paths. eZ/Ibexa image fields can resolve to multiple variations (original, large, small, aliases). The bundle likely picks one alias; if you get full-size originals in the ZIP when you expected thumbnails (or vice versa), look for a configurable alias/imageVariation parameter in DependencyInjection/Configuration.php. This is the most likely config quirk you'll hit.
Permissions are your responsibility. The bundle's route is a plain Symfony route — it does not automatically inherit content read policies. If a user can guess a locationId, they can trigger a ZIP. Add a kernel.request listener or a controller wrapper that checks read on the location before dispatch, and rate-limit the endpoint (ZIP generation is CPU/IO heavy).
Routing prefix conflicts and cache. If the bundle's route path collides with an existing route, you'll see "route already exists" at cache warmup or a 404 at runtime. Override the route by redefining it under your app's routing with a prefix: /custom/download and the bundle's controller as _controller. Always cache:clear after changing bundle registration — stale route caches are the #1 "the bundle isn't working" false alarm.
Debugging a failing download. The ZIP response makes browser devtools unhelpful (you get a broken archive, not an error page). To debug:
var/log/dev.log for ZipArchive::addFile warnings (permission-denied on source files is common if the var/storage path is misconfigured).curl -v -o out.zip <url> and inspect unzip -l out.zip — a valid empty ZIP is 22 bytes, which tells you the loop found zero images (usually a wrong location ID or wrong content type filter).Extension points if you fork. The cleanest customization seams are:
services.yaml (Xrow\DownloadLibraryBundle\… → your class) rather than editing vendor code.Consider building it yourself if you're on Laravel. The feature is genuinely small: query the subtree, resolve image field URIs via your storage abstraction, ZipArchive::addFile each, stream with response()->streamDownload(). That's ~60 lines and zero dependency risk. The only thing you'd import from this bundle is the shape of the route and the button UX — which you can replicate in an hour.
How can I help you explore Laravel packages today?