tyhand/document-downloader-bundle
Tyhand DocumentDownloaderBundle is a Symfony bundle for handling document downloads in your application. It packages the download logic into a reusable bundle, giving controllers and services a consistent way to trigger document file downloads without duplicating code across projects.
This is a 2014-era Symfony bundle with zero stars and no releases since Dec 2014. Treat it as a small reference implementation rather than a maintained dependency — the fastest path is to read the source before installing it.
1. Read the bundle first. Open the repo and scan three files in order:
composer.json — confirms the Symfony version constraint. Expect symfony/symfony: ~2.3 or similar, which means it will not install cleanly on Symfony 4+ without a composer require --ignore-platform-reqs escape hatch (don't do that) or manual porting.DependencyInjection/Extension.php and Configuration.php — reveals the config tree (likely a list of document paths or a documents directory).Controller/* (or Resources/config/routing.yml) — reveals the actual route, download action, and how the filename is validated.2. Register the bundle (Symfony 2/3 style):
// app/AppKernel.php
$bundles = [
// ...
new Tyhand\DocumentDownloaderBundle\TyhandDocumentDownloaderBundle(),
];
3. Import the routing so the download endpoint exists:
# app/config/routing.yml
tyhand_document_downloader:
resource: "@TyhandDocumentDownloaderBundle/Resources/config/routing.yml"
prefix: /documents
4. Configure the document source. Typical shape for this kind of bundle is a directory or named-document map:
# app/config/config.yml
tyhand_document_downloader:
documents_dir: "%kernel.root_dir%/../web/documents"
# or an explicit map, depending on what Configuration.php exposes
# documents:
# terms: "%kernel.root_dir%/../web/documents/terms.pdf"
5. First use case: a link in a Twig template pointing at the bundle route, e.g. {{ path('tyhand_document_download', {'filename': 'terms.pdf'}) }}. Clicking it should return a BinaryFileResponse (or StreamedResponse) with a Content-Disposition: attachment header. That single flow — protected/static PDF or DOCX download from a configured folder — is the entire value proposition.
If you're on Symfony 4+, stop here. Read the controller, copy the ~20 lines of file-serving logic into your own controller, and skip the bundle.
Template link generation over hardcoded URLs. The bundle exists to give you a stable route name instead of /documents/foo.pdf sprinkled through templates. Keep a Twig helper or macro so filename-to-route stays in one place:
{% macro doc_link(filename, label) %}
<a href="{{ path('tyhand_document_download', {'filename': filename}) }}">{{ label }}</a>
{% endmacro %}
Config-driven registry of downloadable documents. Rather than trusting raw filenames from the URL, prefer exposing a named map (if Configuration.php supports it) and having the controller resolve name → path. This is the difference between an open file server and a curated download endpoint. If the bundle only accepts a filename, add your own allowlist in front of it.
Wrap the route in your app's firewall. The bundle predates common access_control conventions in practice, so put the route behind authentication or a voter explicitly:
# app/config/security.yml
access_control:
- { path: ^/documents, roles: ROLE_USER }
For finer control (e.g. only users who own the invoice), don't extend the bundle — route the request through your own controller, run an authorization check, then call the bundle's download logic (or just BinaryFileResponse directly).
Stream large files. If the bundle returns BinaryFileResponse for multi-MB PDFs, you'll want StreamedResponse for anything user-generated. Check the controller — if it uses readfile() inside a BinaryFileResponse, swap to StreamedResponse in your own subclass rather than patching vendor code.
Pair with a versioned assets layout. For documents that change (whitepapers, contracts), store as documents/{slug}/{version}.pdf and pass the resolved path. Combine with Cache-Control: no-store or a short max-age so users always get the current file; the bundle won't set this for you.
Integration tip — feature-flag it. Given zero stars and no maintenance, gate document downloads behind a service or config toggle so you can replace the bundle with a two-line custom controller later without touching templates.
Symfony version lock-in is the headline gotcha. The Dec 2014 release date means the composer.json constraint almost certainly caps at Symfony 2.x/early 3.x. On modern Symfony you'll get dependency conflicts; the right move is to not install it and port the ~20 lines of file-serving code. If you must install on 2.x, pin symfony/symfony explicitly to avoid composer resolving a broken pair.
Abandonment risk. Zero stars, zero dependents, single author, no commits in over a decade. Composer will not warn you about security issues because there are no advisories for it, but it also means no CVE response if a path-traversal bug surfaces. Mitigate by keeping the download directory outside web/ and validating the resolved path:
$real = realpath($documentsDir . '/' . $filename);
if ($real === false || strpos($real, realpath($documentsDir)) !== 0) {
throw new NotFoundHttpException();
}
Path traversal by default. Very likely the controller takes a {filename} route parameter verbatim. Symfony's routing constraints are not applied unless the bundle defines requirements in its routing YAML. Check routing.yml — if there's no requirements: { filename: '[^/]+\.pdf' } (or similar), add an override route in your app with a stricter regex, or wrap it in a controller that validates before delegating.
Filename-to-display-name mismatch. Content-Disposition will likely echo the URL filename. Users downloading invoice-001.pdf see exactly that, not "Invoice January 2024." If the header is set from the path, you can't localize it; you'll need a custom response if presentation matters.
Debugging checklist when a download 404s or 500s:
php app/console debug:router | grep -i document — confirm the route name and pattern actually match what your template generates.php app/console debug:container tyhand — inspect the service and its injected documents_dir, then verify that path exists and is readable by the PHP-FPM user.app/logs/dev.log for BinaryFileResponse throwing FileNotFoundException — it fails at sendContent, so the error may look like an empty response rather than an exception.X-Accel-Redirect isn't expected; typical Symfony responses won't use it, and large files through PHP-FPM may hit client_max_body_size or timeouts unrelated to the bundle.Extension points are thin. This is a "lazy" bundle by its own description — expect no events, no interfaces, no DI tags. Your extension options are effectively: (a) override the routing pattern in your app, (b) override the controller via a service alias, or (c) copy the logic out. Prefer (c). It's a smaller long-term cost than fighting an unmaintained dependency on a modern Symfony.
Config key naming. If Configuration.php uses a hyphenated or snake_case key that doesn't match the README (common in 2014 code), config.yml will silently ignore unknown keys in Symfony 2.x. Run debug:config tyhand_document_downloader to see what the extension actually accepted before assuming the config took effect.
How can I help you explore Laravel packages today?