Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Rest Upload Bundle Laravel Package

srio/rest-upload-bundle

Symfony bundle for handling file uploads over a REST API, offering upload endpoints, configurable storage and validation, and support for multiple files—ideal for adding file-upload capabilities to Symfony applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register (Symfony 2.8/3.x era):

composer require srio/rest-upload-bundle
// app/AppKernel.php
$bundles = [
    // ...
    new SRIO\RestUploadBundle\SRIORestUploadBundle(),
];

Look at these four files first, in this order — they are the whole package:

  1. Upload/UploadHandler.php — the front door; a chain that asks each registered handler "can you handle this request?" and dispatches to the first one that says yes.
  2. Upload/Handler/* — one class per accepted wire format (multipart form data, base64-in-JSON, chunked/partial uploads).
  3. DependencyInjection/Configuration.php + Resources/config/services.xml — the handlers list and the real service ids (confirm these before copy-pasting config; ids and class names shifted between tags).
  4. Tests/ — the most reliable documentation of the exact request shapes each handler expects.

The first use case: one endpoint that accepts a file however the client feels like sending it — multipart/form-data from a classic form today, base64 JSON from a mobile/canvas client tomorrow, chunked uploads later — without touching the controller.

// src/AppBundle/Controller/UploadController.php
public function uploadAction(Request $request)
{
    $result = $this->get('srio_rest_upload.handler')->handle($request, function ($file) {
        $name = bin2hex(random_bytes(8)).'.'.($file->guessExtension() ?: 'bin');
        $file->move($this->getParameter('uploads_dir'), $name);

        return ['id' => $name];   // return scalars/arrays, not entities
    });

    return new JsonResponse($result);
}

That callback is where 90% of your real work lives: validation, naming, storage, and persistence. The bundle only normalises the transport.

Implementation Patterns

Keep the controller a pass-through, put logic in a service. The handler is a normal service; injecting it beats $this->get() (and survives the Symfony 4 migration if you fork):

class AvatarUploader
{
    public function __construct(private UploadHandler $handler, private string $uploadsDir) {}

    public function store(Request $request): array
    {
        return $this->handler->handle($request, function ($file) {
            $this->assertValid($file);            // mime, size, dimensions
            $name = $this->generateName($file);
            $file->move($this->uploadsDir, $name);

            return ['id' => $name, 'url' => '/uploads/'.$name];
        });
    }
}

This makes the upload path unit-testable with mocked handlers, and callable from a console command or message consumer when files arrive out-of-band.

Exploit format polymorphism instead of branching. The point of the package is that the controller is identical for:

// multipart — plain HTML form or FormData
$client->request('POST', '/api/uploads', [], ['file' => $uploadedFile]);

// base64 — SPA / mobile camera payload
$client->request('POST', '/api/uploads', [], [], ['CONTENT_TYPE' => 'application/json'],
    json_encode(['name' => 'avatar.png', 'content' => base64_encode($bytes)]));

// chunked — resumable, PUT-style with Content-Range
$client->request('PUT', '/api/uploads', [], [], [
    'CONTENT_TYPE'  => 'application/octet-stream',
    'CONTENT_RANGE' => 'bytes 0-1048575/5242880',
    'X-File-Name'   => 'video.mp4',
], $chunkBytes);

Write one functional test per format against the same route; that suite is your real safety net if you later swap the package out.

Two-phase / resumable flow. For large files, use the chunked handler as a state machine: each request carries a range and a stable upload identifier; respond with the next expected offset so the client can resume after a dropped connection. The server-side offset is the source of truth — never trust the client's idea of "where it got to".

Persist after assembly, not per chunk. For chunked uploads, write the Upload/Media entity only in the final-chunk branch (when end + 1 === total). Intermediate chunks should touch temp storage only. Encode this as an explicit $context/return value from the callback rather than sniffing headers in your entity listener.

Custom handler for anything the bundle doesn't speak (S3 presigned POST, tus, application/vnd.api+json): implement the handler interface (isSupported(Request) + handle(...)), register it as a service, and prepend it to the handlers list. That is the sanctioned extension point and it's a small interface.

Gotchas and Tips

  • Maintenance risk is the headline. Last release 2016-09-10, ~46 stars, zero dependents. It targets Symfony 2.x/3.0: container-injected controllers via $this->get(), Sensio @Route annotation namespace, UploadedFile::move(). Budget for a fork (namespace + constructor injection + Symfony 4/5 signatures) or plan a replacement — vich/uploader-bundle for storage, oneup/uploader-bundle or tus-php if you need resumability. Do not adopt it greenfield without that exit plan written down.
  • Handler order is semantics, not preference. Chunked uploads often arrive as multipart/form-data too, so a generic multipart handler placed first will swallow them. Put the partial/chunked handler before multipart in the handlers list and verify with a request that only the chunked handler could satisfy.
  • "No handler supports this request" almost always means Content-Type. multipart/form-data; boundary=... vs application/x-www-form-urlencoded vs application/json; also check the client isn't sending Content-Type: text/plain for a raw PUT. Log the resolved handler name at debug level — it saves an hour every time.
  • PHP limits bite the base64 path hardest. Base64 inflates payloads ~33% and the body is decoded in memory: post_max_size, upload_max_filesize, memory_limit, and client_max_body_size (nginx) all matter. Exceeding post_max_size yields an empty $_POST/$_FILES with no error — detect it by comparing $_SERVER['CONTENT_LENGTH'] against the limit in a request listener.
  • Temp chunk files are your problem. Aborted uploads leave partials behind; nothing in the bundle garbage-collects them. Add a cron/console command that deletes temp files older than N hours, and key temp files by upload id, not by client-supplied filename.
  • Never trust the client filename. Path traversal (../../), NUL bytes, and Unicode tricks; guessExtension() and getClientMimeType() are both client-adjacent. Generate your own name, store outside the webroot, and re-validate mime/size after assembly, not per chunk.
  • Parallel chunks need a mutex. Two chunks landing simultaneously on the same upload id will race on the temp file. Filesystem writes aren't atomic; use a lock (flock, Redis, or a DB row SELECT ... FOR UPDATE) around the append.
  • CORS will silently kill PUT/chunked uploads in the browser. Preflight must allow PUT/PATCH and the custom headers (Content-Range, X-File-Name, whatever identifier header you use). NelmioCorsBundle config is the usual fix; test with a real browser, not just curl.
  • Return shapes: the handler generally hands back whatever your callback returns. Returning a Doctrine entity from the callback invites the serializer to walk your object graph — return arrays or a hand-built JsonResponse.
  • Debugging tip: write a throwaway route that echoes $request->getContentType(), $request->headers->all(), and strlen($request->getContent()); reproduce the failing client request once there, then diff against the shapes in Tests/. It's faster than reading the handler chain.
  • Lock the dependency down. With no release since 2016 and no dependents, pin an exact version (or a dev- commit hash), vendor the code if you can, and keep a HandlerInterface shim in your own codebase so a swap is a service definition change rather than a rewrite.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect