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.yaml. File profiles are under files; image profiles are under images and can add image-specific constraints. The YAML payload is mapped into typed UploadConfigDefinition before runtime validation/upload services are built.

example.php
module: upload
config:
  files:
    documents:
      target_directory:
        $env: UPLOAD_DOCUMENT_TARGET_DIRECTORY
        type: string
        default: uploads/documents
      max_bytes:
        $env: UPLOAD_DOCUMENT_MAX_BYTES
        type: int
        default: 10485760
      allowed_mime_types:
        - application/pdf
        - text/plain
      allowed_extensions:
        - pdf
        - txt
  images:
    avatar:
      target_directory:
        $env: UPLOAD_AVATAR_TARGET_DIRECTORY
        type: string
        default: uploads/avatars
      max_bytes:
        $env: UPLOAD_AVATAR_MAX_BYTES
        type: int
        default: 2097152
      allowed_mime_types:
        - image/jpeg
        - image/png
        - image/webp
      allowed_extensions:
        - jpg
        - jpeg
        - png
        - webp
      reencode: true
      min_width: 128
      max_width: 2048
      min_height: 128
      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.

example.php
$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.

example.php
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.

example.php
$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.

example.php
$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.

example.php
module: upload
config:
  images:
    avatar:
      target_directory: uploads/avatars
      max_bytes: 2097152
      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.