crazybooot/base64-validation
Laravel validation rules for Base64-encoded data. Validate Base64 strings, files, and images with MIME type, size, min/max, and image dimension constraints—ideal for JSON APIs and inline file uploads.
Install
composer require crazybooot/base64-validation
The package ships with Laravel package auto-discovery. On Laravel 5.5+ the Base64ValidationServiceProvider is registered automatically and the validation rules become available immediately — no config file to publish, no migration, nothing to wire up. If you are on an older Laravel (or a non-standard skeleton), add Crazybooot\Base64Validation\Base64ValidationServiceProvider::class to config/app.php.
Where to look first
README.md — the rule list and the exact alias names are the only things you truly need. Rule classes live under src/Rules/; the abstract base class is the useful part if you plan to add your own variants.src/Rules/ — reading Base64Image / Base64File tells you how the package decides something is an image or a file, which matters for your error messages and for any edge cases (see Gotchas).The first use case you will actually hit
An API endpoint where a mobile or SPA client posts a JSON body containing a base64 payload instead of a multipart upload:
// app/Http/Requests/StoreAvatarRequest.php
public function rules(): array
{
return [
'avatar' => ['required', 'string', 'base64image', 'max:2048'], // ~2MB of base64 text
];
}
The alternative — hand-rolling base64_decode($value, true) === false plus a MIME sniff — is exactly the boilerplate this package removes. Reach for it the moment you see Content-Type: application/json carrying an image or document.
Pattern 1 — string aliases in a FormRequest (the default path)
The package registers named rules so they compose with the rest of Laravel's string syntax:
return [
'document' => 'required|base64file',
'avatar' => 'nullable|base64image',
'checksum' => 'required|base64',
];
Confirm the exact alias spellings against the README for your installed version before copying them into a large ruleset — alias names have been the one thing that has shifted between releases more than the classes themselves.
Pattern 2 — instantiate the rule object directly
Preferred for anything non-trivial, because it survives alias renames and gives you a type you can inject, unit-test, and decorate:
use Crazybooot\Base64Validation\Rules\Base64Image;
use Crazybooot\Base64Validation\Rules\Base64File;
return [
'avatar' => ['nullable', new Base64Image()],
'document' => ['required', new Base64File()],
];
Rule objects also compose cleanly with Rule::when(), conditionally-applied arrays, and bail.
Pattern 3 — the decode-and-store pipeline
Validation is only half the job; the value arriving is still a string, and often still carrying a data-URI prefix. A tidy controller flow:
$payload = $request->validated('avatar');
// Strip a data URI prefix if the client sent one
$payload = preg_replace('#^data:[\w/+.-]+;base64,#', '', $payload);
$binary = base64_decode($payload, true); // strict decode
$path = 'avatars/'.Str::uuid().'.'.$extension; // derive ext from sniffed MIME
Storage::disk('s3')->put($path, $binary);
Keep the decode in a small action/service class rather than the controller — you will reuse it for every endpoint that accepts base64, and it is the natural place to enforce a byte-size ceiling (see Gotchas).
Pattern 4 — API Resources returning data URIs
If the package is in your stack because clients exchange base64, the outbound side is worth standardising too:
class AvatarResource extends JsonResource
{
public function toArray($request): array
{
return [
'mime' => $this->mime,
'data' => 'data:'.$this->mime.';base64,'.base64_encode(
Storage::get($this->path)
),
];
}
}
Pattern 5 — a reusable Base64Payload value object
For anything beyond a couple of endpoints, wrap the rule and the decode together so the rest of the app never touches a raw base64 string:
final class Base64Payload
{
private function __construct(public readonly string $binary, public readonly string $mime) {}
public static function fromValidated(string $value): self
{
$value = preg_replace('#^data:[\w/+.-]+;base64,#', '', $value);
$binary = base64_decode($value, true);
// finfo / getimagesizefromstring for the MIME
return new self($binary, $mime);
}
}
This is the integration tip that pays for itself: it localises every quirk of base64 handling (padding, prefixes, whitespace) into one class instead of scattering base64_decode calls through controllers.
Testing
The rules are plain ValidationRule objects, so they test without HTTP:
$v = Validator::make(
['avatar' => 'data:image/png;base64,'.base64_encode($pngBytes)],
['avatar' => [new Base64Image()]]
);
$this->assertTrue($v->passes());
Build fixtures with base64_encode(file_get_contents(...)) over tiny committed images — never hand-write base64 in tests.
The data-URI prefix is the #1 tripwire. Browsers' FileReader.readAsDataURL() and most JS canvas APIs return data:image/png;base64,iVBORw0.... Whether the package accepts that prefix (versus only the raw encoded body) is worth verifying with a one-line test before you build a client contract around it. When in doubt, strip it in your FormRequest's prepareForValidation():
protected function prepareForValidation(): void
{
$this->merge([
'avatar' => preg_replace('#^data:[\w/+.-]+;base64,#', '', (string) $this->avatar),
]);
}
Whitespace and newlines silently break validation. base64_encode() in PHP never emits line breaks, but plenty of other tools do (MIME encoding wraps at 76 chars, openssl base64 wraps too, and copy-pasted keys pick up trailing newlines). If you accept externally-produced base64, normalise first:
$value = preg_replace('/\s+/', '', $value);
Otherwise users get a confusing "not valid base64" for a string that decodes perfectly fine.
Strict vs. permissive base64 are different questions. Standard base64 uses + and / and requires = padding to a multiple of four; URL-safe base64 swaps in - and _ and often drops padding entirely (JWTs, some S3 presign styles). Decide which your clients send and validate that. The practical, unambiguous check remains base64_decode($value, true) !== false — consider it your ground truth when a rule and a manual decode disagree.
Watch your size ceiling, not Laravel's. A base64 body is ~33% larger than the binary it encodes, and because these payloads usually arrive as JSON, upload_max_filesize and post_max_size do not apply — the limits that bite are memory_limit, max_input_vars (for form-encoded posts), and your web server's request body cap. Add an explicit max: rule and mirror it as a byte check after decoding. A 5MB image is a 6.7MB string plus a duplicate in memory once decoded.
Image/file validation is heuristic, not a parser. The rules infer type from the decoded header rather than running a full parse. Validate the decoded bytes yourself when it matters — getimagesizefromstring($binary) for images, finfo_buffer() for documents — and treat the rule as a cheap early reject, not a security boundary. Never trust the declared MIME type, and never write the decoded bytes into a web-served directory with a client-supplied extension.
Error messages are unhelpful out of the box. The default message reads like The avatar must be a base64 encoded image. Override it in the FormRequest's messages() array or via lang/*/validation.php using the rule alias as the key. Do this early — the generic message sends users down the wrong debugging path when the real problem is a missing prefix or a stray newline.
Extension points. The rules share an abstract base; if you need a narrower variant (say, "PNG only, max 1MB, no animation"), subclass it rather than stacking regex on the controller. Register your alias in a Validator::extend() call inside a service provider if you want the string syntax to match the package's.
Debugging recipe. When a rule fails unexpectedly, log the first ~40 characters of the value and its length. That immediately distinguishes the three real causes — prefix present, whitespace present, or truncated/odd-length string — from a genuine encoding error. Add the log line behind a config flag so it doesn't leak payloads in production.
Non-issue to skip. There is no config file, no cache to clear, and no publish step. If a rule alias reports as unknown, the service provider isn't loaded (common after manual Laravel upgrades that regenerate bootstrap/cache/packages.php) — run php artisan package:discover and retry before assuming the package is broken.
How can I help you explore Laravel packages today?