eduplus / cdn
Laravel Storage driver and standalone PHP client for mdscdn
Requires
- php: >=7.2
- guzzlehttp/guzzle: ^7.2
Suggests
- illuminate/support: Required only if you use the Laravel service provider to register Storage::disk('eduplus'). Not needed for the standalone CdnClient.
This package is auto-updated.
Last update: 2026-08-16 11:18:18 UTC
README
PHP client + Laravel filesystem driver for mdscdn. Two ways to use it:
Storage::disk('eduplus')— drop-in Laravel Storage driver. Every existingStorage::call (put,get,delete,exists,copy,move,files,allFiles,url,temporaryUrl,setVisibility, etc.) works exactly like it does againstlocalors3.Eduplus\Cdn\CdnClient— a standalone class with no Laravel dependency at all, for any PHP project (or a Laravel job/console command that wants direct control without going through the Storage facade).
Built specifically for the mdscdn server API — see that project's README for the server side of this contract.
Installation
composer require eduplus/cdn
Laravel auto-discovers CdnServiceProvider via the extra.laravel
block in composer.json — nothing to register manually.
Option 1 — Laravel Storage::disk('eduplus')
Add a disk to config/filesystems.php:
'disks' => [ // ...existing disks... 'eduplus' => [ 'driver' => 'eduplus', 'base_url' => env('EDUPLUS_CDN_URL', 'https://cdn.abcd.com'), 'bucket' => env('EDUPLUS_CDN_BUCKET'), 'api_key' => env('EDUPLUS_CDN_API_KEY'), 'timeout' => env('EDUPLUS_CDN_TIMEOUT', 15), 'verify_ssl' => env('EDUPLUS_CDN_VERIFY_SSL', true), ], ],
.env:
EDUPLUS_CDN_URL=https://cdn.abcd.com
EDUPLUS_CDN_BUCKET=acme-media
EDUPLUS_CDN_API_KEY=sk_live_xxxxxxxx
Then use it exactly like any other disk:
use Illuminate\Support\Facades\Storage; Storage::disk('eduplus')->put('logos/school.png', $fileContents); Storage::disk('eduplus')->put('logos/school.png', $fileContents, [ 'visibility' => 'private', 'max_age' => 3600, 'hashed' => true, ]); Storage::disk('eduplus')->get('logos/school.png'); Storage::disk('eduplus')->exists('logos/school.png'); Storage::disk('eduplus')->delete('logos/school.png'); Storage::disk('eduplus')->copy('a.png', 'b.png'); Storage::disk('eduplus')->move('a.png', 'archive/a.png'); Storage::disk('eduplus')->makeDirectory('reports/2026'); Storage::disk('eduplus')->deleteDirectory('reports/2026'); Storage::disk('eduplus')->files('reports/2026'); Storage::disk('eduplus')->allFiles('reports'); Storage::disk('eduplus')->directories('reports'); Storage::disk('eduplus')->url('logos/school.png'); Storage::disk('eduplus')->temporaryUrl('private/doc.pdf', now()->addMinutes(15)); Storage::disk('eduplus')->setVisibility('doc.pdf', 'private'); Storage::disk('eduplus')->size('logos/school.png'); Storage::disk('eduplus')->lastModified('logos/school.png'); Storage::disk('eduplus')->mimeType('logos/school.png');
Want it as the app's default disk? Set FILESYSTEM_DISK=eduplus in
.env and skip ->disk('eduplus') everywhere — plain Storage::put(...)
goes straight to the CDN.
Upload options
Pass these as the third argument to put() (they map to the Config
object Flysystem gives the adapter):
| Option | Meaning |
|---|---|
visibility |
'public' (default) or 'private' |
hashed |
true to get a random hashed filename back instead of the one requested |
max_age |
Override Cache-Control: max-age (seconds) for this specific file |
Option 2 — standalone CdnClient (no Laravel required)
use Eduplus\Cdn\CdnClient; $cdn = new CdnClient( 'https://cdn.abcd.com', // base URL 'acme-media', // bucket 'sk_live_xxxxxxxx' // api key ); // Upload $result = $cdn->upload('logos/school.png', file_get_contents('logo.png')); echo $result['url']; // https://cdn.abcd.com/acme-media/logos/school.png // Upload a local file directly (streams it, doesn't load into memory) $cdn->uploadFile('/tmp/report.pdf', 'reports/2026/q1.pdf', ['visibility' => 'private']); // Read $contents = $cdn->read('logos/school.png'); $stream = $cdn->readStream('reports/2026/q1.pdf'); // Existence & metadata $cdn->exists('logos/school.png'); $cdn->meta('logos/school.png'); // size, last_modified, mime_type, checksum, visibility $cdn->size('logos/school.png'); $cdn->lastModified('logos/school.png'); $cdn->mimeType('logos/school.png'); $cdn->checksum('logos/school.png'); // Delete / copy / move $cdn->delete('logos/school.png'); $cdn->copy('a.png', 'b.png'); $cdn->move('a.png', 'archive/a.png'); // Directories $cdn->makeDirectory('reports/2026'); $cdn->deleteDirectory('reports/2026'); $cdn->files('reports/2026', recursive: true); $cdn->directories('reports', recursive: true); // Visibility & URLs $cdn->setVisibility('doc.pdf', 'private'); $cdn->getVisibility('doc.pdf'); $cdn->url('logos/school.png'); // unsigned public URL $cdn->temporaryUrl('doc.pdf', 900); // signed URL, expires in 900s // Health check $cdn->healthCheck(); // bool — hits GET /healthz
Every method throws Eduplus\Cdn\Exceptions\CdnException on failure
(network error or non-2xx response) — call $e->statusCode() to branch on
the HTTP status if needed.
Why read() works for private files too
CdnClient::read() and readStream() internally mint a short-lived
signed URL (via the same mechanism as temporaryUrl()) and fetch through
it, rather than hitting the plain public URL. That means a valid API key
is all you need to read a file server-side, regardless of whether it was
uploaded as public or private — the visibility setting only affects
unauthenticated browser access to the plain url().
Requirements
- PHP >= 7.2 — written to run unmodified from 7.2 through current PHP (8.x).
- ext-json
guzzlehttp/guzzle^7.2 (installed automatically via Composer)league/flysystem^2.0 (on PHP 7.2–7.4) or ^3.0 (on PHP 8.0.13+) — Composer resolves the right one automatically per project; both versions share the sameFilesystemAdaptercontract this package is written againstilluminate/support— only if using the Laravel service provider; the standaloneCdnClienthas no Laravel dependency
Notes / current limitations
directoryExists()in the Flysystem adapter distinguishes "exists" from "missing" by whether listing the path 404s — same limitation any plain filesystem has around telling an empty directory apart from a missing one.- Multipart uploads aren't used by this client —
upload()always sends the file as a raw PUT body, which the mdscdn server accepts natively for both string content and streams. - Before relying on this in production, run
composer installand a quick smoke test against a real mdscdn instance (upload → read → delete) — ideally under your oldest supported PHP version (7.2) as well as current, since this package was written and syntax-checked (php -lon every file, PHP 8.3) for 7.2+ compatibility but wasn't executed against a live installed copy ofleague/flysystem/guzzlehttp/guzzlein the environment it was authored in.