spe/filesize-extension-bundle
A Symfony bundle that provides a Twig extension for formatting file sizes into human-readable strings. Register the bundle and use the filter in templates to display byte values as KB, MB, GB, and more.
Install
composer require spe/filesize-extension-bundle
There is no Flex recipe, so register the bundle manually in config/bundles.php:
return [
// ...
Spe\FilesizeExtensionBundle\FilesizeExtensionBundle::class => ['all' => true],
];
No configuration file is required — the bundle only registers a Twig extension.
Where to look first
Before writing templates, confirm the public surface in vendor/spe/filesize-extension-bundle/:
src/Twig/ or src/Extension/) and read getFilters(). That tells you the exact filter name (filesize in most versions), its callable, and any accepted arguments (precision, unit, etc.).composer.json for the twig/twig and symfony/* constraints. With 11 stars and no dependents, the constraints may predate Symfony 6/7 or Twig 3 — this is the first thing to verify on a modern app.bin/console debug:twig --filter=filesize
First use case
You have a byte count (from filesize(), an entity column, or Flysystem metadata) and want 1.24 MB instead of 1298432:
// Controller
$documents = array_map(fn (SplFileInfo $f) => [
'name' => $f->getFilename(),
'size' => $f->getSize(), // raw int — never format before sorting
], iterator_to_array($finder));
{# templates/documents/list.html.twig #}
{% for doc in documents %}
<li>{{ doc.name }} — {{ doc.size|filesize }}</li>
{% endfor %}
If the filter works there, you are done. Everything below is about doing it consistently.
Keep raw bytes, format at the edge. The single most important pattern: store/transport integers and apply |filesize only in the template. Formatting in the controller or entity destroys sortability, breaks CSV/API exports, and makes tests assert on presentation instead of data.
<td data-sort="{{ file.size }}">{{ file.size|filesize }}</td>
Map filesystem/Flysystem objects in the controller. Twig cannot call Flysystem\FileAttributes::fileSize() with arguments and lazy adapters should not be hit inside loops in the template. Build a small view model:
$files = [];
foreach ($storage->listContents('uploads') as $item) {
$files[] = ['path' => $item->path(), 'size' => $item->fileSize()];
}
Pass precision explicitly if the filter supports it. If the signature is filesize(int $bytes, int $precision = 2), standardise it once via a Twig global or a wrapper macro so every screen renders 1.24 MB and not a mix of 1.2 MB / 1.243 MB:
{# templates/_macros.html.twig #}
{% macro size(bytes) %}{{ bytes|filesize(2) }}{% endmacro %}
Normalise in one place when the filter is not enough. If you need locale-aware units (KB vs Ko, 1,24 MB in fr_FR), decorate rather than fork. Register your own extension that delegates:
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
final class LocalizedFilesizeExtension extends AbstractExtension
{
public function __construct(private /* the bundle's extension */ $inner) {}
public function getFilters(): array
{
return [new TwigFilter('filesize_localized', [$this, 'format'])];
}
public function format(int $bytes, int $precision = 2): string
{
return str_replace('.', ',', $this->inner->format($bytes, $precision)) . ' ' . "\u{00A0}";
}
}
Alternatively use Twig's twig.extension tag with a distinct filter name — safer than overriding filesize globally, which silently changes every template.
Use it outside Twig via the same service. If the extension's formatter is a standalone class, inject that; if it is not, do not call Twig from a controller. For PDFs, emails, and CLI output, extract the formatting logic into App\Formatter\FilesizeFormatter and have both the Twig filter and the CLI command use it. This is the common exit ramp from a single-purpose bundle.
Admin panels (EasyAdmin/Sonata). Format at render time in a field configurator/template rather than persisting a formatted string:
yield Field::new('size')->formatValue(fn ($v) => $this->twig->createTemplate('{{ v|filesize }}')->render(['v' => $v]));
Prefer a dedicated Twig template partial over createTemplate if you hit caching or sandbox issues.
Testing. Assert on the filter output directly rather than through HTTP where possible:
$twig = self::getContainer()->get('twig');
$out = $twig->createTemplate('{{ 1298432|filesize }}')->render([]);
self::assertSame('1.24 MB', $out);
One functional test through a real template catches the registration problem (bundle in bundles.php, cache cleared) that unit tests miss.
Cache/serialisation integration. If you cache rendered fragments, key the cache on the raw bytes, not on the formatted string — otherwise a precision change requires a full cache purge.
Binary vs decimal is the trap. Most filesize-style filters divide by 1024 but label the result KB/MB. That is technically KiB/MiB. If you display storage quotas, billing, or anything contractual, confirm the divisor in the source and consider using kB/MB (1000) semantics — or write your own ten-line filter. Do not discover this from a support ticket.
Verify the filter signature, do not assume it. debug:twig --filter=filesize is faster than reading a template failure. The argument order and whether precision is passed as |filesize(1) or |filesize(precision=1) varies between minor versions of small packages.
Zero and failed filesize() calls. filesize() returns false (and emits a warning) on unreadable files. A filter typed int will throw a TypeError under strict types; an untyped filter will render 0 B or false. Guard at the boundary:
'size' => is_readable($path) ? (filesize($path) ?: 0) : 0,
Strings from the database. A column stored as VARCHAR returning "1,024" will fail numeric coercion. Cast in the entity (int) rather than relying on PHP's loose comparison — the filter may use intdiv/round internally and silently truncate.
Locale and decimal separators. Expect 1.24 MB with a dot regardless of locale. If the app is multilingual, wrap with |replace({'.': ','}) at minimum, and be aware the unit letters (MB, GB) are not translatable by this bundle. That is the usual reason teams outgrow it.
Symfony/Twig version drift. With no dependents and a low star count, compatibility is on you. Check the composer.json constraints; on Symfony 6.4/7 with Twig 3, the extension may still work (the AbstractExtension API is stable) but the bundle class may use removed getContainerExtension patterns. If bin/console fails after install, composer why-not spe/filesize-extension-bundle and a two-line service registration in config/services.yaml is usually enough to bypass the bundle entirely:
services:
Spe\FilesizeExtensionBundle\Twig\FilesizeExtension:
tags: ['twig.extension']
Cache clearing after config changes. Adding the bundle or changing a tag requires bin/console cache:clear in dev and a deploy-time cache:clear in prod; a missing filter in prod while dev works is almost always a warm-cache issue.
Bus factor. Eleven stars, no dependents, MIT license. The whole feature set is one class. Pin the version in composer.json, and if you need localisation, SI units, or a specific precision policy, vendor a 30-line Twig extension instead of fighting the package — the migration cost is near zero either way.
Extension points to know. There are likely none exposed (no parameters, no formatter service contract). The realistic customisation route is service decoration of the Twig extension or registering a parallel filter name. Do not monkey-patch vendor/ — a composer install erases it.
Do not |raw the output. It is plain text with no markup; |raw only creates an XSS surface if the input ever becomes user-controlled. Also avoid double-escaping by not combining it with |escape inside an already-escaped block.
Performance is a non-issue. The filter is arithmetic and string concatenation; a foreach over a few thousand files costs nothing compared to the filesystem stats. Do not prematurely cache formatted strings.
How can I help you explore Laravel packages today?