silarhi/rest-upload-bundle
A Symfony bundle for adding RESTful file-upload endpoints to your application. It streamlines receiving, storing, and managing uploaded files through a simple API, reducing boilerplate for upload workflows.
Reality check: this is a Symfony bundle, not a Laravel package. No ServiceProvider, no facade, no publishable config/rest-upload.php. Day-to-day, a Laravel developer will only touch it in two real scenarios:
multipart/form-data on one endpoint.Given 0 stars, 0 dependents, and no CVE surface, do not add it to composer.json for a Laravel app. Read the source, copy the pattern.
In a Symfony app — the minimal path:
composer require silarhi/rest-upload-bundle
Symfony Flex has no recipe for it, so register it manually:
// config/bundles.php
return [
// ...
SRIO\RestUploadBundle\SRIORestUploadBundle::class => ['all' => true],
];
Then read, in this order, before writing any controller:
src/ — the request listener (this is the whole product) and the uploaded-file factory.DependencyInjection/Configuration.php — this is the true config surface; the README is thinner than the tree.Content-Type and converts the request body into a Symfony\Component\HttpFoundation\File\UploadedFile.That last piece is the point. The bundle's job is: JSON {"file": "<base64>"} → UploadedFile, so that standard File/Image/MimeType constraints and $file->move() work unchanged in your controller.
First use case — a single upload endpoint that accepts either shape:
#[Route('/api/upload', methods: ['POST', 'PUT'])]
public function upload(Request $request): JsonResponse
{
// The bundle has already normalized the request:
// - multipart → $request->files->get('file')
// - base64 JSON → also $request->files->get('file')
// - raw binary → also $request->files->get('file')
$file = $request->files->get('file');
if (!$file instanceof UploadedFile) {
return $this->json(['error' => 'No file provided'], 400);
}
$file->move($this->getParameter('uploads_dir'), $file->getClientOriginalName());
return $this->json(['path' => '/uploads/' . $file->getClientOriginalName()], 201);
}
In Laravel — the equivalent first step: write a middleware (NormalizeUploadRequest) that checks Content-Type; if application/json, base64-decode $request->input('file') to a temp path and call UploadedFile::createFromBase(); if application/octet-stream, do the same with $request->getContent(). Then everything downstream — FormRequest validation, Storage::disk('uploads')->putFile(), Image rules — stays exactly as your team already writes it. The pattern is worth 10 lines of middleware; the bundle is not worth a framework detour.
Pattern 1 — Format dispatch behind a single route. The bundle's core idea: one /upload resource, three body shapes, one controller. In Laravel, implement it as middleware that normalizes to a UploadedFile before the request reaches the controller:
// app/Http/Middleware/NormalizeUpload.php
public function handle(Request $request, Closure $next)
{
if ($request->isMethod('POST') || $request->isMethod('PUT')) {
$ct = $request->header('Content-Type', '');
if (str_contains($ct, 'application/json') && $b64 = $request->input('file')) {
$tmp = tempnam(sys_get_temp_dir(), 'up_');
file_put_contents($tmp, base64_decode($b64, true));
$request->files->set('file', new UploadedFile($tmp, 'upload', null, null, true));
} elseif (str_starts_with($ct, 'application/octet-stream')) {
$tmp = tempnam(sys_get_temp_dir(), 'up_');
file_put_contents($tmp, $request->getContent());
$request->files->set('file', new UploadedFile($tmp, 'upload', null, null, true));
}
}
return $next($request);
}
Register it in bootstrap/app.php or Kernel::middleware where it runs before your FormRequest validation. That's the whole "pattern transfer."
Pattern 2 — Keep validation in your existing stack. The bundle does not add a validation DSL; it relies on Symfony's File/Image constraints and MimeType. The Laravel analogue is unchanged: 'file' => ['required', 'file', 'max:10240', 'mimes:jpg,png,pdf']. Do not re-implement validation in the dispatch layer — normalize first, validate second, store third.
Pattern 3 — Storage abstraction, not local move(). The bundle's move()-to-directory default is fine for a demo; it is a scaling blocker in production. In Laravel, always Storage::disk('s3')->putFile('uploads', $file) via Flysystem. In Symfony, swap the "write file" step for a Flysystem service. The dispatch logic stays untouched.
Pattern 4 — Handler-per-format for teams. If you expect more shapes (chunked/resumable, remote-URL fetch, zipped batches), define:
interface UploadHandler {
public function supports(Request $request): bool;
public function toUploadedFile(Request $request): UploadedFile;
}
…and iterate a tagged collection in the middleware. That's the structure the bundle hints at but does not enforce; putting it in explicitly is what makes it maintainable.
Pattern 5 — Testing. With normalization as middleware, your existing feature test:
$file = UploadedFile::fake()->image('avatar.jpg');
$this->postJson('/api/upload', ['file' => base64_encode(file_get_contents($file))])
->assertCreated();
gives you coverage of the JSON path for free. Add one test per Content-Type variant; that's the entire test surface.
memory_limit and post_max_size explicitly, and enforce a hard per-request cap in middleware before you decode anything. The bundle does not protect you here; validation runs after the string is in memory.base64_decode with strict=true. Silent corruption from a trailing newline or data-URI prefix (data:image/png;base64,) is a common bug. Strip the prefix, then strict-decode; reject invalid payloads with a 400 before constructing UploadedFile.$file->getClientOriginalName() is user-controlled and can contain ../ or null bytes. Never pass it straight to move() or a storage path. Use a generated name (Str::uuid() + safe extension) and keep the original only in metadata.Content-Type: image/png with a PHP payload. Always re-check the real MIME via finfo (Laravel's mimes/mimetypes rules already do this for UploadedFile created via createFromBase — verify your middleware sets it consistently).try/finally and register a reaper (cron or Laravel's schedule) for aborted-request orphans. This is the #1 silent operational failure of upload endpoints.config/bundles.php line means the listener never runs and $request->files->get('file') silently returns null for JSON requests. Symptom looks like a validation bug; the cause is a missing registration.php://input exhaustion. Confirm the bundle's listener runs at a sensible priority before yours.UploadedFile from JSON? Check test: mode in createFromBase. When constructing UploadedFile manually, the 5th argument ($test = true) suppresses is_uploaded_file() failures — required for tests and for streamed bodies, easy to forget, and the resulting error message (The file ... was not uploaded) is unhelpful.tus or S3 multipart presigned, do not bend this pattern — go straight to the protocol. The single-endpoint dispatch is a solution for body shapes, not for large or resumable uploads.How can I help you explore Laravel packages today?