Tested and maintained, so you don't have to. $99/year.
Upload
Advanced file upload service with automatic image optimization, format conversion, multiple size generation, and cloud storage support.
Why Use This Service?
What you'd build yourself: File validation, MIME type verification, image processing with GD/Imagick, format conversion, multiple size generation, secure filename handling, and S3/DigitalOcean Spaces integration.
Maintained for you: image library quirks and storage API changes get handled here, not in your app.
What Pro provides:
- Real MIME type validation (not just extensions)
- Image resizing with fit/cover/contain modes
- WebP conversion for smaller files
- Named conversion presets (thumbnail, medium, large)
- S3 and DigitalOcean Spaces with temporary URLs
- FileInfo and FileCollection objects for clean API
Installation
Flick Pro requires a license. See Pro installation for setup instructions.
Configuration
Basic configuration requires only a storage directory:
$config = [ 'services' => [ 'upload' => [ 'directory' => '/path/to/uploads', 'url' => 'https://example.com/uploads' // Base URL for files ] ] ]; $form = new Flick($config);
Full Configuration
$config = [ 'services' => [ 'upload' => [ 'directory' => '/path/to/uploads', 'url' => 'https://example.com/uploads', 'maxFileSize' => '10MB', // Global max size 'timeout' => 60, // Processing timeout in seconds 'overwrite' => false, // Overwrite existing files // Predefined image conversions 'conversions' => [ 'thumbnail' => [ 'width' => 150, 'height' => 150, 'resizeMode' => 'cover', 'format' => 'webp', 'quality' => 80 ], 'medium' => [ 'width' => 600, 'height' => 400, 'resizeMode' => 'contain', 'quality' => 85 ], 'large' => [ 'width' => 1200, 'resizeMode' => 'fit', 'quality' => 90 ] ] ] ] ];
Cloud Storage (S3)
Cloud storage needs two packages Flick Pro does not install for you — the AWS SDK they pull in is large, and most sites store files locally:
composer require league/flysystem league/flysystem-aws-s3-v3
Without them, resolving an S3 storage config throws with that same command in the message. Local storage needs nothing extra.
$config = [ 'services' => [ 'upload' => [ 'directory' => '/tmp/uploads', // Temp directory for processing 'storage' => [ 'driver' => 's3', 'bucket' => 'my-bucket', 'region' => 'us-east-1', 'key' => $_ENV['AWS_ACCESS_KEY_ID'], 'secret' => $_ENV['AWS_SECRET_ACCESS_KEY'], 'acl' => 'public-read', // public URLs; omit for private objects 'cdnUrl' => 'https://cdn.example.com' // optional custom public base URL ] ] ] ];
DigitalOcean Spaces
'storage' => [ 'driver' => 's3', 'bucket' => 'my-space', 'region' => 'nyc3', 'endpoint' => 'https://nyc3.digitaloceanspaces.com', 'key' => $_ENV['DO_SPACES_KEY'], 'secret' => $_ENV['DO_SPACES_SECRET'], 'acl' => 'public-read' ]
Public URLs are built from cdnUrl when set, otherwise from the endpoint (or
the standard regional S3 URL). Without 'acl' => 'public-read', objects are stored
private and getUrl() returns signed, expiring URLs instead of plain public ones.
Basic Usage
Upload an Image
$form->create('Photo|file'); if ($form->submitted()) { $file = $form->upload->image('photo', ['required']); if ($file) { $form->successMessage("Uploaded: {$file->url}"); } }
Upload a PDF
$file = $form->upload->pdf('document', [ 'required', 'maxFileSize:10MB' ]);
Upload Any File
$file = $form->upload->file('attachment', [ 'required', 'maxFileSize:5MB', 'mime:text/plain,application/pdf,application/zip' ]);
Warning
file() requires a mime: rule. Leave it out and the upload is rejected with
"A mime type must be specified for file uploads." — file() accepts anything, so
Flick makes you say what "anything" means. image() and pdf() have their own
built-in allowlists and don't need it.
PHP has its own upload limits
maxFileSize can't raise PHP's own ceilings — a stock PHP install caps each file
at 2 MB (upload_max_filesize) and the whole request at 8 MB (post_max_size).
If uploads fail before Flick ever sees them, raise those two values in php.ini.
Multiple files share the post_max_size budget, so five 5 MB files need a 25 MB+
request limit.
Image Processing
Resize Images
$file = $form->upload->image('photo', [ 'width:800', 'height:600' ]);
Format Conversion
Convert images to WebP or other formats:
$file = $form->upload->image('photo', [ 'format:webp', 'quality:85' ]);
Resize Modes
Control how images are resized:
// Cover - Fill dimensions, crop excess $file = $form->upload->image('photo', [ 'width:400', 'height:400', 'resizeMode:cover' ]); // Contain - Canvas is exactly 800x600; image scaled to fit inside, rest padded $file = $form->upload->image('photo', [ 'width:800', 'height:600', 'resizeMode:contain' ]); // Fit - Scale proportionally; output is the scaled image, no padding (default) $file = $form->upload->image('photo', [ 'width:1200', 'resizeMode:fit' ]); // Stretch - Force exact dimensions, ignoring aspect ratio (may distort) $file = $form->upload->image('photo', [ 'width:400', 'height:400', 'resizeMode:stretch' ]);
cover, contain and stretch need both width and height. fit works
with either one on its own.
Preserve Original
Keep the original file alongside the processed version:
$file = $form->upload->image('photo', [ 'width:800', 'format:webp', 'preserveOriginal' ]); // Creates: photo.webp (processed) and photo-original.jpg (original)
Adjustments
Three rules take a value and can be repeated — pass the same rule twice and both adjustments apply, in the order you wrote them.
| Rule | Description | Example |
|---|---|---|
brightness:N |
Brightness, -100 to 100 |
brightness:20 |
contrast:N |
Contrast, -100 to 100 |
contrast:15 |
gamma:N |
Gamma correction, 1.0 leaves it unchanged |
gamma:1.2 |
$file = $form->upload->image('photo', [ 'width:800', 'brightness:10', 'contrast:15' ]);
Effects
| Rule | Description | Example |
|---|---|---|
blur:N |
Blur radius; repeatable | blur:5 |
sharpen:N |
Sharpen amount; repeatable | sharpen:10 |
grayscale |
Convert to grayscale | grayscale |
sepia |
Sepia tone | sepia |
invert |
Invert colors | invert |
flip:DIRECTION |
horizontal or vertical |
flip:horizontal |
rotate:DEGREES |
Rotate by degrees | rotate:90 |
background:COLOR |
Background behind transparency | background:#ffffff |
grayscale, sepia and invert are bare flags with no value, and apply once
even if repeated.
$file = $form->upload->image('photo', [ 'width:600', 'grayscale', 'blur:3' ]);
background accepts a hex color (#ffffff or #fff), an (r, g, b) triple, or
one of transparent, white, black, red, green, blue.
Processing order is fixed
Rules are applied in a set order regardless of how you list them: resize, background, then adjustments (brightness, contrast, gamma), then effects (blur, sharpen, grayscale, sepia, invert, flip, rotate). Only repeats of the same rule run in the order you wrote them.
Thumbnails
thumbnails writes extra fixed-size copies next to the processed file, each
cropped to fill its dimensions. Sizes are WIDTHxHEIGHT, comma-separated:
$form->upload->image('photo', [ 'width:1200', 'thumbnails:150x150,300x300' ]); // Writes photo.jpg, photo_150x150.jpg and photo_300x300.jpg
The thumbnails are written to disk but are not returned on the result object. For sizes you need to reference later — a thumbnail URL to put in a template — use conversions instead, which name each output and hand it back to you.
Conversions
Generate multiple sizes/formats from a single upload using predefined conversions.
Define Conversions in Config
$config = [ 'services' => [ 'upload' => [ 'directory' => '/uploads', 'conversions' => [ 'thumbnail' => [ 'width' => 150, 'height' => 150, 'resizeMode' => 'cover', 'format' => 'webp', 'quality' => 80 ], 'medium' => [ 'width' => 600, 'quality' => 85 ], 'large' => [ 'width' => 1200, 'quality' => 90 ] ] ] ] ];
Use Conversions
// Apply multiple conversions $files = $form->upload->image('photo', [ 'conversions:thumbnail,medium,large' ]); // Returns FileCollection with all versions echo $files->original->url; // Main processed file echo $files->thumbnail->url; // Thumbnail version echo $files->medium->url; // Medium version echo $files->large->url; // Large version
Single Conversion
Apply a single predefined conversion. The conversion's settings are applied to
the uploaded file, and this also returns a FileCollection (not a bare
FileInfo), so read the result through ->original:
$files = $form->upload->image('photo', [ 'conversion:thumbnail' ]); echo $files->original->url;
Override Conversion Settings
$files = $form->upload->image('photo', [ 'conversion:thumbnail', 'quality:95' // Override quality from config ]);
Validation Rules
Common Rules
| Rule | Description | Example |
|---|---|---|
required |
File must be uploaded | required |
maxFileSize:SIZE |
Maximum file size | maxFileSize:5MB |
mimeTypes:TYPES |
Narrow the handler's allowlist | mimeTypes:image/jpeg,image/png |
mime:TYPES |
Allowed MIME types for file() — required there |
mime:application/zip |
blockedMimeTypes:TYPES |
Reject these types even if otherwise allowed | blockedMimeTypes:image/gif |
thumbnails:SIZES |
Extra cropped copies, WxH comma-separated |
thumbnails:150x150,300x300 |
mimeTypes and mime are different rules, not aliases. Use mimeTypes with
image() or pdf() to narrow what that handler already allows; use mime with
file(), which has no allowlist of its own and won't run without one.
Image Rules
| Rule | Description | Example |
|---|---|---|
width:PX |
Resize width | width:800 |
height:PX |
Resize height | height:600 |
format:TYPE |
Output format | format:webp |
quality:1-100 |
Output quality | quality:85 |
resizeMode:MODE |
Resize behavior | resizeMode:cover |
preserveOriginal |
Keep original file | preserveOriginal |
conversion:NAME |
Apply single conversion | conversion:thumbnail |
conversions:NAMES |
Apply multiple conversions | conversions:thumb,medium |
Resize Modes
| Mode | Description |
|---|---|
fit |
Scale proportionally to fit within the dimensions; the output canvas is the scaled image itself, so it may be smaller than the box (default) |
cover |
Fill dimensions exactly, crop excess |
contain |
Output canvas is exactly the given dimensions; the image is scaled to fit inside and the leftover space is padded. Requires both width and height |
stretch |
Resize to exact dimensions, ignoring aspect ratio. Requires both width and height |
Naming Rules
Each upload is stored in its own subdirectory named after the file — photo.jpg
lands at uploads/photo/photo.jpg, with any conversions alongside it. Set
'flat' => true in the upload config to store files directly in the upload
directory instead.
Leave the rule off and Flick keeps the uploaded filename, sanitized. Add a name:
rule to pick a different strategy:
// Random hash — 12 characters by default $file = $form->upload->image('photo', ['name:hash']); // Random hash, custom length $file = $form->upload->image('photo', ['name:hash,16']); // 9b483583754b8aa6.jpg // Today's date $file = $form->upload->image('photo', ['name:date']); // 2026-08-08.jpg // Date and time $file = $form->upload->image('photo', ['name:time']); // 2026-08-08-03-21-55.jpg // Anything else is used literally as the filename $file = $form->upload->image('photo', ['name:avatar']); // avatar.jpg
| Strategy | Result |
|---|---|
| (rule omitted) | The uploaded filename, sanitized — photo.jpg, then photo_1.jpg on collision |
name:hash |
12 random hex characters |
name:hash,N |
N random hex characters |
name:date |
Y-m-d |
name:time |
Y-m-d-H-i-s |
name:anythingelse |
Used verbatim as the filename |
Warning
There's no uuid strategy, and name:original is not special — both fall into the
last row and produce files literally called uuid.jpg and original.jpg. The
name: rule never sets the extension — the stored extension follows the file's
processed format: images are re-encoded, so a format: rule changes the extension
to match, and a BMP upload is re-encoded and stored as .jpg.
Predictable names + overwrite
The default naming produces guessable paths (the sanitized upload name, with
_1/_2 suffixes on collision). If you also set overwrite => true, one user
uploading avatar.png will replace another user's avatar.png. For user-supplied
files, use name:hash for unpredictable names, and scope upload directories per
user rather than relying on overwrite.
FileInfo Object
Successful uploads return a FileInfo object:
$file = $form->upload->image('photo', ['required']); if ($file) { $file->name; // Generated filename $file->originalFilename; // Original upload name $file->path; // Storage path $file->url; // Public URL $file->size; // Size in bytes $file->mimeType; // MIME type $file->extension; // File extension }
FileCollection Object
Conversion rules (conversion: and conversions:) return a FileCollection:
$files = $form->upload->image('photo', [ 'conversions:thumbnail,medium' ]); // Access by name $files->original; // Main processed file (FileInfo) $files->thumbnail; // Thumbnail conversion (FileInfo) $files->medium; // Medium conversion (FileInfo) $files->getConversion('original'); // Preserved original if preserveOriginal (FileInfo|null) // Get all as array (keyed by conversion name) $allFiles = $files->getAllFiles(); foreach ($allFiles as $name => $fileInfo) { echo "{$name}: {$fileInfo->url}\n"; }
getAllFiles() puts the main processed file under the key original. When
preserveOriginal is on, the untouched upload appears alongside it under
preservedOriginal, and you can also reach it with
$files->getConversion('original'). It is not listed by
getConversionNames() — that lists conversions only.
Cloud Storage
Temporary URLs
Generate expiring URLs for private files:
// Get URL that expires in 60 minutes $url = $form->upload->getTemporaryUrl('path/to/file.pdf', 60);
Tip
Only cloud storage can expire a URL. On local storage this returns the plain public URL with no expiry, so the call is safe to leave in place while you develop.
Check Available Conversions
// List all defined conversions $conversions = $form->upload->getAvailableConversions(); // Check if specific conversion exists if ($form->upload->hasConversion('thumbnail')) { // ... }
Methods Reference
upload->image()
Upload and process image files.
$form->upload->image( string $name, array $rules = [], array $messages = [] ): FileInfo|FileCollection|array|null
| Parameter | Type | Description |
|---|---|---|
$name |
string |
Form field name |
$rules |
array |
Validation/processing rules |
$messages |
array |
Custom error messages |
Allowed MIME types: image/jpeg, image/png, image/gif, image/webp, image/bmp
upload->pdf()
Upload PDF files.
$form->upload->pdf( string $name, array $rules = [], array $messages = [] ): FileInfo|FileCollection|array|null
Allowed MIME types: application/pdf
upload->file()
Upload generic files.
$form->upload->file( string $name, array $rules = [], array $messages = [] ): FileInfo|FileCollection|array|null
Requires a mime: rule listing the allowed MIME types (see Common Rules) — without one, the upload fails with "A mime type must be specified for file uploads."
upload->getTemporaryUrl()
Generate expiring URL for cloud storage.
$form->upload->getTemporaryUrl( string $remotePath, int $expiresInMinutes = 60 ): string
On failure it returns an empty string and records the reason in the error bag
under the upload key. Local storage has no signing mechanism, so it returns
the plain file URL.
upload->getAvailableConversions()
Get the names of every conversion defined in your config.
$form->upload->getAvailableConversions(): array // ['thumbnail', 'medium', 'large']
upload->hasConversion()
Check if a conversion is defined.
$form->upload->hasConversion(string $name): bool
Complete Example
<?php require 'vendor/autoload.php'; $form = new Flick\Flick([ 'services' => [ 'upload' => [ 'directory' => __DIR__ . '/uploads', 'url' => 'https://example.com/uploads', 'maxFileSize' => '10MB', 'conversions' => [ 'thumbnail' => [ 'width' => 200, 'height' => 200, 'resizeMode' => 'cover', 'format' => 'webp', 'quality' => 80 ], 'gallery' => [ 'width' => 800, 'height' => 600, 'resizeMode' => 'contain', 'format' => 'webp', 'quality' => 85 ] ] ], 'sql' => [ 'driver' => 'mysql', 'host' => 'localhost', 'database' => 'myapp', 'username' => 'root', 'password' => 'secret' ] ] ]); $db = $form->sql; $form->create('Title, Description|textarea, Image|file'); if ($form->submitted()) { $request = $form->request(' Title[required, min:3], Description[required, min:10] '); // Upload image with multiple conversions $files = $form->upload->image('image', [ 'required', 'maxFileSize:5MB', 'conversions:thumbnail,gallery', 'preserveOriginal' ]); if ($form->ok() && $files) { // Save to database $form->sql->save('posts', [ 'title' => $request['title'], 'description' => $request['description'], 'image_original' => $files->getConversion('original')?->path, 'image_thumbnail' => $files->thumbnail->path, 'image_gallery' => $files->gallery->path, 'image_processed' => $files->original->path ]); $form->successMessage('Post created successfully!'); } }
Security Features
- MIME Type Validation: Actual file type is verified, not just extension
- File Size Limits: Configurable size limits prevent large uploads
- Secure File Names: Uploaded filenames are sanitized or replaced
- Directory Traversal Prevention: Paths are validated and sanitized