DEMO / LiveComponent

Uploading files

File uploads are tricky. Submit them to a #[LiveAction] with the files modifier on data-live-action then process them.

Note: files aren't persisted and will be lost on each rerender if not stored.

Current file: none
Current files: (none)
// ... use statements hidden - click to show
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;

#[AsLiveComponent]
class UploadFiles
{
    use DefaultActionTrait;

    public function __construct(private ValidatorInterface $validator)
    {
    }

    #[LiveProp]
    public ?string $singleUploadFilename = null;
    #[LiveProp]
    public ?string $singleFileUploadError = null;

    #[LiveProp]
    public array $multipleUploadFilenames = [];

    #[LiveAction]
    public function uploadFiles(Request $request): void
    {
        $singleFileUpload = $request->files->get('single');
        if ($singleFileUpload) {
            $this->validateSingleFile($singleFileUpload);
        }

        if ($singleFileUpload instanceof UploadedFile) {
            [$this->singleUploadFilename] = $this->processFileUpload($singleFileUpload);
        }

        $multiple = $request->files->all('multiple');
        foreach ($multiple as $file) {
            if ($file instanceof UploadedFile) {
                [$filename, $size] = $this->processFileUpload($file);
                $this->multipleUploadFilenames[] = ['filename' => $filename, 'size' => $size];
            }
        }
    }

    private function processFileUpload(UploadedFile $file): array
    {
        // in a real app, move this file somewhere
        // $file->move(...);

        return [$file->getClientOriginalName(), $file->getSize()];
    }

    private function validateSingleFile(UploadedFile $singleFileUpload): void
    {
        $errors = $this->validator->validate($singleFileUpload, [
            new Assert\File([
                'maxSize' => '1M',
            ]),
        ]);

        if (0 === \count($errors)) {
            return;
        }

        $this->singleFileUploadError = $errors->get(0)->getMessage();

        // causes the component to re-render
        throw new UnprocessableEntityHttpException('Validation failed');
    }
}
<div class="container" {{ attributes }}>
    <p class="text-muted"><strong>Note:</strong> files aren't persisted and will be lost on each rerender if not stored.</p>
    <div class="form-group mb-3">
        <label for="single-file" class="form-label">Single file:</label>
        <input
            type="file"
            name="single"
            autocomplete="off"
            id="single-file"
            class="form-control-file {{ singleFileUploadError ? 'is-invalid' : '' }}"
        >
        <div class="form-text text-muted">Current file:
        {% if singleUploadFilename %}
            {{ singleUploadFilename }}
        {% else %}
            none
        {% endif %}
        </div>
        {% if singleFileUploadError %}
            <div class="invalid-feedback">{{ singleFileUploadError }}</div>
        {% endif %}
    </div>
    <div class="form-group mb-3">
        <label for="multiple-files" class="form-label">Multiple files:</label>
        <input type="file" name="multiple[]" autocomplete="off" multiple="" id="multiple-files" class="form-control-file">
        <div class="form-text text-muted">Current files:
        {% if multipleUploadFilenames %}
        <ul>
            {% for file in multipleUploadFilenames %}
                <li>{{ file.filename }} ({{ file.size }} bytes)</li>
            {% endfor %}
        </ul>
        {% else %}
            (none)
        {% endif %}
        </div>
    </div>
    <button
        data-action="live#action"
        data-live-action-param="files|uploadFiles"
        class="btn btn-primary"
    >Upload files!</button>
</div>