Core

Upload Service Provider

PSR-7 upload factory, validators, storage and profile-based file/image uploads.

Responsibility

Registers typed upload services: FileUploadValidator, ImageUploadValidator, UploadStorage, MimeTypeDetector, GdImageProcessor, UploadService and UploadFactory. The provider does not register a string alias upload; controllers access uploads through $this->upload() and services should inject UploadFactory.

Upload config

Application upload profiles live in app/Config/Upload.php. File profiles are under files.profiles; image profiles are under images.profiles and can add image-specific constraints.

<?php

declare(strict_types=1);

use Lemonade\Framework\Support\Env;

return [
    'files' => [
        'profiles' => [
            'documents' => [
                'target_directory' => 'uploads/documents',
                'max_bytes' => Env::int('UPLOAD_DOCUMENT_MAX_BYTES', 10 * 1024 * 1024),
                'allowed_mime_types' => ['application/pdf', 'text/plain'],
                'allowed_extensions' => ['pdf', 'txt'],
            ],
        ],
    ],
    'images' => [
        'profiles' => [
            'avatar' => [
                'target_directory' => 'uploads/avatars',
                'max_bytes' => Env::int('UPLOAD_AVATAR_MAX_BYTES', 2 * 1024 * 1024),
                'allowed_mime_types' => ['image/jpeg', 'image/png', 'image/webp'],
                'allowed_extensions' => ['jpg', 'jpeg', 'png', 'webp'],
                'reencode' => true,
                'min_width' => 128,
                'min_height' => 128,
                'max_width' => 2048,
                'max_height' => 2048,
            ],
        ],
    ],
];

PSR-7 input

Upload processing starts from PSR-7 UploadedFileInterface values in ServerRequestInterface::getUploadedFiles(). UploadFactory can resolve a named input from the current request, including nested names supported by UploadedFileResolver.

$uploadedFiles = $request->getUploadedFiles();
$file = $uploadedFiles['avatar'] ?? null;

$image = $container
    ->get(UploadFactory::class)
    ->image('avatar')
    ->uploadFromRequest($request, 'avatar');

Controller usage

Framework controllers expose $this->upload() through AbstractController. Catch UploadException around upload handling and decide in the application how to render or redirect after success/failure.

use Lemonade\Framework\Upload\Exception\UploadException;

try {
    $image = $this->upload()->uploadImage('avatar', 'avatar');

    $storedPath = $image->storedRelativePath();
} catch (UploadException $e) {
    $error = $e->getMessage();
}

Factory API

UploadFactory is the high-level API. Use file($profile) / image($profile) for configured uploaders, upload($inputName, $profile) / uploadImage($inputName, $profile) for current-request shortcuts, or fileWithOptions() / imageWithOptions() when options are created manually.

$document = $this->upload()->upload('document', 'documents');
$image = $this->upload()->uploadImage('avatar', 'avatar');

$rules = $this->upload()->image('avatar')->rules();

Upload result

UploadedFile contains storedFilename(), storedPath(), storedRelativePath(), mimeType() and sizeBytes(). UploadedImage adds width() and height(). The result does not expose the original client filename; persist stored metadata instead.

$payload = [
    'stored_filename' => $image->storedFilename(),
    'stored_path' => $image->storedPath(),
    'stored_relative_path' => $image->storedRelativePath(),
    'mime_type' => $image->mimeType(),
    'size_bytes' => $image->sizeBytes(),
    'width' => $image->width(),
    'height' => $image->height(),
];

Validation

FileUploadValidator validates payload presence, PHP upload error, temporary file path, non-empty size, max bytes, detected MIME type and client extension. ImageUploadValidator reuses file validation and adds getimagesize() readability plus optional min/max width and height. Failed validation throws UploadValidationException.

Storage behavior

UploadFactory resolves target_directory through ApplicationContext::resolveUploadPath() and also keeps a portable relative path via uploadRelativePath(). UploadStorage creates the target directory, generates a random stored filename with the resolved extension, moves or writes the file, and reports final size.

Image processing

Image uploads use GdImageProcessor. When profile reencode is true, the image is re-encoded to the stored path using the detected MIME type. Stored image metadata is then read from the saved file.

'images' => [
    'profiles' => [
        'avatar' => [
            'target_directory' => 'uploads/avatars',
            'allowed_mime_types' => ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
            'allowed_extensions' => ['jpg', 'jpeg', 'png', 'webp', 'gif'],
            'reencode' => true,
            'max_width' => 1024,
            'max_height' => 1024,
        ],
    ],
],

Error handling

Upload APIs throw exceptions instead of returning an error result object. Catch UploadException for framework upload failures; specific subclasses include validation, storage, processing and image processing exceptions.

Security notes

Do not trust original filenames or extensions alone. Use profile MIME and extension constraints, rely on generated stored filenames, and persist only stored metadata. Keep upload targets under framework upload storage unless you intentionally expose selected files through a controlled public path.