aimeos/ai-filesystem
Aimeos ai-filesystem provides a flexible filesystem abstraction for the Aimeos e-commerce framework, letting you store and serve product media on local disks or cloud storage such as AWS S3 via Flysystem, with consistent file access, URLs, and previews.
First, a triage step. aimeos/ai-filesystem is the filesystem abstraction layer from the Aimeos 2015.x–2016.x era: a thin manager over league/flysystem ~1.0 that hands out named disks (media, tmp, etc.) to the rest of the shop. It was last released in February 2016, has no dependents, and the functionality now ships inside aimeos/aimeos-core under the Aimeos\Base\Filesystem namespace. So:
aimeos/aimeos-core / aimeos/aimeos-laravel; the manager API is the same shape.aimeos/aimeos-core. Check with composer why aimeos/ai-filesystem before touching anything.If you are on a legacy install and need to work with it, read three files first:
src/MW/Filesystem/Manager.php — the registry. get('media') builds (and memoizes) one disk from config.src/MW/Filesystem/Fly.php — the Flysystem bridge; this is where return values, exceptions, and path handling are decided.src/MW/Filesystem/Fly/* — the concrete adapters (Local, S3, …) and the constructor keys each one expects.Configuration lives in the Aimeos config under an fs block:
// config/shop.php — illustrative; verify key names against Manager::get()
'fs' => [
'media' => [
'adapter' => 'Local',
'root' => base_path('public/uploads'),
],
'private' => [
'adapter' => 'S3',
'bucket' => env('AWS_BUCKET'),
'key' => env('AWS_KEY'),
'secret' => env('AWS_SECRET'),
'region' => env('AWS_REGION'),
],
],
The exact keys (
rootvsbasedir,keyvsaccesskey) shifted between minor releases. OpenManager::get()and read which keys it forwards to the adapter constructor before copying any snippet from a blog post.
First use case: storing a product image. In a job or admin controller, pull the disk off the context and write a stream:
$fs = $this->getContext()->getFilesystemManager()->get('media');
if (!$fs->has('p/1.jpg')) {
$fs->write('p/1.jpg', fopen($sourcePath, 'r'));
}
That is the entire day-to-day surface: get() a named disk, then read, readf, write, writef, has, remove, move, copy, size, time.
Treat the manager as a lazy registry, not a service locator. Every get('media') returns the same instance per request, constructed on first access. That makes it cheap to call, but it also means the disk is effectively immutable once built — don't try to flip config mid-request and expect a different root.
Prefer the file-path helpers in batch code. read() returns an open stream and write() expects one you own. In long-running queue workers, hand-written fopen() calls leak file descriptors until you hit ulimit. Use readf($path) / writef($path, $localPath) where they exist so the wrapper does the open/close for you.
Wrap it once, inject everywhere. Calling app('aimeos.context')->getFilesystemManager() from a dozen controllers couples your Laravel code to Aimeos internals. Bind a small adapter instead:
$this->app->singleton(MediaStore::class, function ($app) {
return new MediaStore($app['aimeos.context']->getFilesystemManager());
});
Then swap the binding in tests for an in-memory or temp-dir implementation.
Fan out by concern, not by adapter. Name disks after what the data is for — media (public product images), private (order PDFs, exports), tmp (import staging) — and give each its own root or bucket. This is the cheapest way to keep invoice PDFs off a web-served directory, and it means migrating to S3 later is a config change per disk rather than a code change.
Keep the disk and the URL prefix in sync. The filesystem only stores blobs; Aimeos media items store the relative path, and public URLs are produced elsewhere (the media item's URL prefix or your own config). If you move media from public/uploads to an S3 bucket, you must change both the disk config and the URL base, or you'll write files successfully and serve broken images — a failure mode that looks like a filesystem bug but isn't.
S3 pattern for local development: keep the S3 adapter in fs/private but override the whole block via an environment-conditional config so tests and php artisan serve hit a Local root in storage/. Never let a test suite reach a real bucket; with a dead, unmaintained dependency you don't want accidental deletes.
Testing: point the disk root at sys_get_temp_dir()/aimeos-test and delete it in tearDown. The Flysystem v1 Local adapter requires the root directory to already exist — it creates subdirectories but not the root.
Dead dependency, live risk. No release since 2016-02-18, zero dependents, LGPL-3.0. It pins league/flysystem ^1.0, which is incompatible with Flysystem 2.x/3.x (write() became writeStream(), booleans became exceptions, mount() disappeared). If any other package in the tree pulls a modern Flysystem, expect either a hard Composer conflict or a runtime "method not found". The correct fix is migrating to the Aimeos\Base\Filesystem layer in aimeos/aimeos-core, which is close enough in API that a namespace search-and-replace (Aimeos\MW\ → Aimeos\Base\) plus a Flysystem version check usually compiles.
PHP compatibility. The code targets PHP 5.5–7.0. On PHP 8 you'll hit fatals from removed constructs (curly-brace string offsets) and a wall of deprecations. There is no upstream fix coming.
Failures are false, not exceptions. Flysystem v1 adapters return false on failure. write() returning falsy, remove() silently doing nothing, has() returning false for a permissions problem rather than a missing file — all real. Check return values on every write and delete; do not assume the operation happened.
Path semantics differ per adapter. Leading slashes, .. segments, backslashes, and empty directory placeholders behave differently on Local vs S3. Normalize to forward-slash, relative, no-leading-slash paths at your boundary and never pass user-supplied paths through untouched.
Debugging is bare. There's no logging, no event dispatcher, no exception context. The fastest diagnostic is to wrap the disk in a decorator that logs every method call and its return value, then bind that in place of the raw manager result.
Extension point. The manager resolves adapters by the adapter key, so a custom adapter class is a legitimate hook for cross-cutting concerns — retries around S3, logging, virus scanning before write(). Keep any such class in your own package, not in a fork; LGPL-3.0 permits linking from proprietary code, but modifications to the library itself must be published.
Config cache trap. Because disk instances are memoized per manager, a config change made after first get() is ignored — including in long-running workers where the manager may be constructed once at boot. Rebuild the manager (or the container binding) in tests, and in workers prefer restarting over mutating config at runtime.
Don't web-serve the root. Whatever you point media at, ensure the directory is served as static files only — no PHP execution — since the layer will happily write whatever bytes you give it under whatever name you give it.
How can I help you explore Laravel packages today?