blackcube / yii-fileprovider
PHP 8.3+ File provider with prefix routing and image processing for Yii3 framework
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/blackcube/yii-fileprovider
Requires
- php: >=8.3
- league/flysystem: ^3.31
Requires (Dev)
- codeception/codeception: ^5.3
- codeception/module-asserts: ^3.3
- codeception/module-filesystem: ^3.0
- codeception/module-phpbrowser: ^4.0
- codeception/module-rest: ^3.4
- intervention/image: ^3.11
- league/flysystem-aws-s3-v3: ^3.31
- league/flysystem-ftp: ^3.31
- league/flysystem-sftp-v3: ^3.31
- nyholm/psr7: ^1.8
- nyholm/psr7-server: ^1.1
- vlucas/phpdotenv: ^5.6
- yiisoft/aliases: ^3.1
- yiisoft/data-response: ^2.1
- yiisoft/di: ^1.4
- yiisoft/http: ^1.3
- yiisoft/test-support: ^3.1
Suggests
- intervention/image: Required for image processors (resize, watermark, etc.)
- league/flysystem-aws-s3-v3: Required for S3/MinIO storage
- league/flysystem-ftp: Required for FTP storage
- league/flysystem-sftp-v3: Required for SFTP storage
- yiisoft/aliases: For resolving Yii path aliases in filesystem configuration
This package is auto-updated.
Last update: 2026-01-25 19:08:49 UTC
README
⚠️ Blackcube Warning
This is not a Flysystem wrapper. It's a multi-filesystem router with image processing.
You write
@blfs/image.jpg, FileProvider routes to S3. You chain->resize(300, 200)->read(), it processes on the fly. You never touch Flysystem directly. You never juggle adapters.
PHP 8.3+ multi-filesystem file provider with prefix routing, image processing and Resumable.js upload for Yii3 framework.
Installation
composer require blackcube/yii-fileprovider
Optional dependencies:
# S3/MinIO support composer require league/flysystem-aws-s3-v3 # Image processing composer require intervention/image # FTP/SFTP support composer require league/flysystem-ftp composer require league/flysystem-sftp-v3
Requirements
- PHP >= 8.3
- Flysystem 3.x
Why FileProvider?
| Approach | Problem |
|---|---|
| Raw Flysystem | One adapter = one filesystem. Need S3 + local? Manage two adapters. |
| Multiple adapters | Which one handles /tmp/upload.jpg? Which one handles /cdn/image.jpg? |
| Manual routing | if (str_starts_with($path, '@tmp')) everywhere |
| Image processing | Read, process, write back. Three operations for one resize. |
| FileProvider | None of the above |
You use path prefixes. @bltmp/file.jpg → temporary storage. @blfs/file.jpg → permanent storage. FileProvider routes automatically.
Cross-filesystem operations are transparent. ->move('@bltmp/upload.jpg', '@blfs/final.jpg') — local to S3, S3 to local, whatever. One method.
Image processing is fluent. ->resize(300, 200)->greyscale()->read('@blfs/image.jpg'). Chain what you need.
Resumable uploads work out of the box. Chunks, resume, preview, delete. Three actions, zero boilerplate.
How It Works
Prefix Routing
| Column | Purpose |
|---|---|
@bltmp |
Temporary storage (uploads, chunks) |
@blfs |
Permanent storage (final files) |
@blcdn |
CDN storage (public assets) |
You define the prefixes. You assign a filesystem to each. FileProvider does the rest.
$provider = new FileProvider([ '@bltmp' => new FlysystemLocal('/tmp/uploads'), '@blfs' => new FlysystemAwsS3(bucket: 'my-bucket', ...), ]); // Routes to local $provider->write('@bltmp/upload.jpg', $content); // Routes to S3 $provider->move('@bltmp/upload.jpg', '@blfs/images/photo.jpg');
Image Processing
Fluent API. Chain processors. Read or write.
// Resize on read $resized = $provider ->resize(300, 200) ->read('@blfs/image.jpg'); // Chain processors $processed = $provider ->resize(800, null) ->greyscale() ->quality(85) ->read('@blfs/image.jpg'); // Process on write $provider ->resize(1920, 1080) ->watermark('/path/to/logo.png', 'bottom-right', 10) ->write('@blfs/image.jpg', $contents);
Driver auto-detection: vips > imagick > gd. Override if needed.
Quick Start
1. Configure FileProvider
use Blackcube\FileProvider\FileProvider; use Blackcube\FileProvider\FlysystemLocal; use Blackcube\FileProvider\FlysystemAwsS3; $provider = new FileProvider([ '@bltmp' => new FlysystemLocal('/path/to/tmp'), '@blfs' => new FlysystemAwsS3( bucket: 'my-bucket', key: 'ACCESS_KEY', secret: 'SECRET_KEY', region: 'eu-west-1', ), ]);
2. Use it
// Write $provider->write('@bltmp/upload.jpg', $content); // Read $content = $provider->read('@blfs/images/photo.jpg'); // Move across filesystems $provider->move('@bltmp/upload.jpg', '@blfs/images/photo.jpg'); // Copy $provider->copy('@blfs/original.jpg', '@bltmp/backup.jpg'); // Delete $provider->delete('@blfs/old-file.jpg'); // Check existence if ($provider->fileExists('@blfs/image.jpg')) { // ... } // Check if path is handled if ($provider->canHandle('@blfs/file.jpg')) { // ... }
Available Adapters
| Adapter | Description | Requires |
|---|---|---|
FlysystemLocal |
Local filesystem | - |
FlysystemAwsS3 |
AWS S3 / MinIO | league/flysystem-aws-s3-v3 |
FlysystemFtp |
FTP | league/flysystem-ftp |
FlysystemSftp |
SFTP | league/flysystem-sftp-v3 |
S3-Compatible Storage (MinIO)
$s3 = new FlysystemAwsS3( bucket: 'my-bucket', key: 'minioadmin', secret: 'minioadmin', region: 'us-east-1', endpoint: 'http://localhost:9000', pathStyleEndpoint: true, // Required for MinIO );
Image Processing
Requires intervention/image.
| Method | Description |
|---|---|
resize(?int $width, ?int $height) |
Scale proportionally |
crop(int $width, int $height, ?int $x, ?int $y) |
Crop to dimensions |
rotate(float $angle) |
Rotate counterclockwise |
flip(string $direction) |
Mirror (horizontal or vertical) |
greyscale() |
Convert to greyscale |
blur(int $amount) |
Apply gaussian blur (0-100) |
watermark(string $image, string $position, int $padding) |
Add watermark |
quality(int $quality) |
Set output quality (0-100) |
format(string $format) |
Convert format (jpg, png, webp, etc.) |
Force Driver
$provider = new FileProvider( filesystems: ['@blfs' => $fs], defaultAlias: '@blfs', imageDriver: 'gd', // Default: auto-detection vips > imagick > gd );
Resumable.js Upload
Chunked upload with resume support. Three actions, ready to use.
Configuration
// config/params.php 'blackcube/yii-fileprovider' => [ 'resumable' => [ 'tmpPrefix' => '@bltmp', 'chunkSize' => 524288, // 512 KB 'uploadEndpoint' => '/fileprovider/upload', 'previewEndpoint' => '/fileprovider/preview', 'deleteEndpoint' => '/fileprovider/delete', 'filetypeIconAlias' => '@fileprovider/filetypes/', 'thumbnailWidth' => 200, 'thumbnailHeight' => 200, ], ],
Actions
| Action | Method | Description |
|---|---|---|
ResumableUploadAction |
GET | Test if chunk exists (resume) |
ResumableUploadAction |
POST | Upload chunk |
ResumablePreviewAction |
GET | Preview / thumbnail / icon |
ResumableDeleteAction |
DELETE | Delete file (@bltmp only) |
Upload Flow
Browser (Resumable.js)
│
├─► GET /upload?resumable* → 200 (exists) / 204 (no)
│
▼ POST /upload (multipart + chunk)
ResumableUploadAction
│
▼ saveChunk()
ResumableService ──► @bltmp/{identifier}/{filename}.part{n}
│
▼ isComplete() → assemble()
@bltmp/{filename}
│
▼ Form submit (business logic)
FileProvider->move('@bltmp/...', '@blfs/...')
│
▼
@blfs/, @blcdn/, etc.
Security
| Protection | Mechanism |
|---|---|
| Path traversal filename | cleanFilename() removes ../, ..\\, .. |
| Path traversal delete | deleteTmpFile() only allows @bltmp/ |
| Flysystem detection | PathTraversalDetected → 403 Forbidden |
DI Configuration
// config/common/di.php use Blackcube\FileProvider\FileProvider; use Blackcube\FileProvider\Contracts\FileProviderInterface; use Blackcube\FileProvider\FlysystemLocal; use Blackcube\FileProvider\FlysystemAwsS3; use Yiisoft\Aliases\Aliases; return [ FileProviderInterface::class => static function (Aliases $aliases): FileProviderInterface { return new FileProvider([ '@bltmp' => new FlysystemLocal($aliases->get('@runtime/tmp')), '@blfs' => new FlysystemAwsS3( bucket: $_ENV['S3_BUCKET'], key: $_ENV['S3_KEY'], secret: $_ENV['S3_SECRET'], region: $_ENV['S3_REGION'], ), ]); }, ];
Let's be honest
Image processing is not free
Each ->resize() or ->greyscale() reads the full image into memory, processes it, and outputs. For a 20MB photo, that's 20MB+ in memory per request.
In practice: Thumbnails on upload? Fine. On-the-fly processing for every request? Use a CDN with edge processing.
Cross-filesystem moves are not atomic
->move('@bltmp/file.jpg', '@blfs/file.jpg') = read + write + delete. If write fails, the source remains. If delete fails after write, you have duplicates.
In practice: For critical data, verify destination exists before deleting source.
Resumable.js is for uploads, not downloads
The chunked protocol handles uploads. For large downloads, use signed URLs or streaming.
Tests
# All tests (unit + functional) make test # Unit tests only make test-unit # Functional tests only (starts HTTP server) make test-functional # Clean artifacts make clean
License
BSD-3-Clause. See LICENSE.md.
Author
Philippe Gaultier pgaultier@blackcube.io