1tomany/data-uri-bundle
Symfony bundle wrapping 1tomany/data-uri: auto-configures a denormalizer for TemporaryFileInterface objects and adds a console command to encode files as base64 data URIs. No configuration needed.
First, the reality check for a Laravel shop. This is a Symfony bundle, not a Laravel package. It ships no service provider, no facade, no config file, and Composer auto-discovery won't do anything for you. There are two legitimate ways a Laravel developer ends up here:
composer require 1tomany/data-uri — and skip the bundle entirely. The bundle adds exactly two things: a DataUriNormalizer wired into Symfony's Serializer, and the onetomany:data-uri:encode-file console command. Both are ~50 lines of glue you can reimplement idiomatically in Laravel.Symfony path (if you're actually on Symfony):
composer require 1tomany/data-uri-bundle
With Flex the bundle registers itself in config/bundles.php. Without Flex, add:
OneToMany\DataUriBundle\DataUriBundle::class => ['all' => true],
There is no configuration. The README is explicit: the denormalizer is auto-tagged. Don't go looking for config/packages/data_uri.yaml — it doesn't exist and the bundle has no Configuration tree.
Where to look first (in this order, because the README won't tell you):
vendor/1tomany/data-uri-bundle/src/ — the whole bundle is tiny; read Serializer/DataUriNormalizer.php and the service definition file (usually config/services.php or Resources/config/services.yaml).vendor/1tomany/data-uri/src/Contract/Record/DataUriInterface.php — this is the contract you'll be type-hinting against. Read the record implementations next to it; they're what you'll actually instantiate.First use case — smoke test before you wire anything up. The console command is the fastest way to confirm the library behaves on your files (filenames with unicode, weird MIME sniffing, symlinks):
bin/console onetomany:data-uri:encode-file public/img/logo.png
# data:image/png;base64,iVBORw0KGgo...
Take that output, paste it into a Blade template or a JSON response, and confirm the browser renders it. That's the entire value proposition in one line. Then decide whether the Serializer integration is worth wiring.
Laravel path — the 20-line equivalent. Don't add a dependency for base64_encode. If all you need is the CLI/one-off behaviour:
$mime = (new \finfo(FILEINFO_MIME_TYPE))->file($path);
$uri = sprintf('data:%s;base64,%s', $mime, base64_encode(file_get_contents($path)));
Reach for 1tomany/data-uri only when you want the DataUriInterface record abstraction — i.e. you're passing typed objects around rather than strings, and you want parsing/validation of incoming data URIs for free instead of a hand-rolled regex.
The bundle's real job. In your DTO, type-hint the property against the interface; Symfony's Serializer will turn a data:image/png;base64,... string from a JSON request body into an object.
use OneToMany\DataUri\Contract\Record\DataUriInterface;
use Symfony\Component\Serializer\Attribute\SerializedName;
final class CreateAttachment
{
#[SerializedName('file')]
public DataUriInterface $dataUri; // accepts "data:...;base64,..."
}
This is the pattern that makes the buy-vs-build argument: the client posts an inline image, you get a typed object, no controller-side decoding. It shines for small admin-panel thumbnails and for headless-CMS workflows where the media lives in S3 but the API needs to echo it back inline.
As of v0.0.8 the normalizer is tagged with both serializer.normalizer and serializer.denormalizer. So the same object round-trips: DTO in → serialized string out. Older versions only did one direction; if you're pinned below 0.0.8, don't assume symmetry.
$json = $serializer->serialize($attachment, 'json');
// {"file":"data:application/pdf;base64,JVBERi0xLjQK..."}
Inline small things, link big ones. Do not put this branching inside the DTO — wrap it in a decorator or a dedicated service so the DTO stays a dumb value object:
final readonly class ThumbnailResolver
{
private const MAX_INLINE_BYTES = 64 * 1024;
public function __construct(private Encoder $encoder) {}
public function resolve(string $path): string
{
if (filesize($path) > self::MAX_INLINE_BYTES) {
return $this->cdn->url($path); // fall back to a URL
}
return (string) $this->encoder->encode($path);
}
}
Threshold in the tens of KB, not MB. See Gotchas for why.
Base64 output is deterministic for a given file, so never encode the same bytes twice per request cycle, and ideally not twice per deploy:
$key = 'datauri:'.sha1_file($path);
$uri = Cache::rememberForever($key, fn () => (string) $encoder->encode($path));
Invalidate by content hash, not by timestamp — that makes it idempotent across servers.
If you're porting the idea to Laravel, these are the four seams that map 1:1 to what the bundle does:
| Symfony concept | Laravel equivalent |
|---|---|
DataUriNormalizer (denormalize) |
FormRequest validation + a DataUri cast on the model |
DataUriNormalizer (normalize) |
JsonResource::toArray() calling the encoder |
onetomany:data-uri:encode-file |
php artisan make:command or a queued job |
| Serializer auto-tagging | Explicit binding in a ServiceProvider |
A custom Eloquent cast gives you the round-trip for near-free:
class DataUriCast implements CastsAttributes
{
public function get($model, $key, $value, $attributes): ?DataUriInterface
{
return $value === null ? null : $this->encoder->decode($value);
}
public function set($model, $key, $value, $attributes): ?string
{
return $value instanceof DataUriInterface ? (string) $value : $value;
}
}
The encode command is more useful in CI than in a terminal by hand: generate inline icons for an email template, pre-bake critical CSS assets, or seed fixtures with deterministic data URIs. Wrap it in a Composer script or a deploy step rather than asking developers to paste output into templates.
bin/console debug:container --tag=serializer.normalizer. The bundle ships no priority config, so collisions are resolved by registration order — a classic silent-failure source.#[ApiProperty] with an example value and an OpenAPI schema override, or your generated docs will claim the field is a plain string of unbounded length.The repo is archived. This is the single most important fact and it appears nowhere in the README. Pin the version explicitly in composer.json, and accept that bug fixes are yours to make. The bundle is small enough that forking or vendoring src/ is genuinely viable — read the license, it's MIT.
Zero stars, zero dependents. There is no community to answer questions. When something breaks, the debugging path is vendor/1tomany/data-uri-bundle/src → vendor/1tomany/data-uri/src. Budget for that; don't assume a StackOverflow answer exists.
v0.1.0 pins 1tomany/data-uri to exactly 6.0.7. That's a hard lock, not a range. If your project has any constraint touching that package, expect a solver conflict, and expect to have to reason about it rather than just bumping.
The 33% size tax compounds fast. Base64 inflates payloads by a third, then JSON escaping adds more, then gzip recovers some but not all (base64 compressed ~= original compressed, so you lose most of the savings). Concrete failure modes:
memory_limit blowups on base64_encode of large files — it needs the whole file in memory, plus the output, plus the string copy.client_max_body_size and fastcgi_buffers on the response side for large inline payloads.Rule of thumb: inline icons, thumbnails, logos, small SVGs. Link everything else.
Data URIs are invisible to the browser cache. This defeats the usual "inlining reduces requests" argument for anything loaded more than once. Inline a favicon once in a layout — fine. Inline a 40 KB hero image on every page — you've traded one cacheable request for 40 KB of uncacheable HTML on every navigation.
Security, specifically:
href, src, iframe, or CSS url(). data:text/html;base64,... executes in the document context in the right (wrong) circumstances. Allowlist image/* MIME prefixes at minimum.finfo.Error handling is sparse. "File not found" from the encode path may surface as a bare exception without the path or a useful code, per the TPM notes. Wrap calls in try/catch, log the resolved absolute path yourself, and translate to a 422 rather than a 500:
try {
$record = $this->encoder->encode($path);
} catch (\Throwable $e) {
throw ValidationException::withMessages(['file' => 'File could not be encoded.']);
}
No configuration tree = no extension points, but you can still decorate. The bundle exposes no options. To change behaviour, decorate rather than fork:
#[AsDecorator(decorates: OneToMany\DataUriBundle\Serializer\DataUriNormalizer::class)]
final class SafeDataUriNormalizer implements NormalizerInterface, DenormalizerInterface
{
public function __construct(private NormalizerInterface&DenormalizerInterface $inner) {}
// delegate, but reject non-image/* or oversized payloads
}
That's the cleanest lever: MIME allowlisting, size caps, and logging all belong in a decorator, not in your DTOs.
Debugging recipe for "the field is null." Three suspects, in order: (1) the property isn't typed against DataUriInterface so the denormalizer never fires; (2) the normalizer was overridden by a project normalizer with higher priority; (3) the DTO's property isn't settable/public and you're not using a constructor. Check (2) with debug:container --tag=serializer.normalizer, then dd($serializer->getSupportedTypes('json')).
Test the bytes, not the string. Asserting on the full base64 blob makes tests brittle and unreadable. Assert the prefix (data:image/png;base64,) and that base64_decode(substr($uri, strpos($uri, ',') + 1)) matches the fixture file's contents.
The honest recommendation for a Laravel-only team: don't adopt this. The value is the typed DataUriInterface round-trip, and you can build that in an afternoon with a cast, a JsonResource, and a FormRequest rule. Adopt it if you're on Symfony and already using the Serializer (API Platform especially) — the zero-config auto-tagging genuinely saves you from hand-wiring a normalizer. Skip it if you need streaming, transformation, or long-term maintenance guarantees; the archived status plus the pinned dependency is a real operational cost for a package whose core is base64_encode.
How can I help you explore Laravel packages today?