codewiser / simple-files
Light-weight file manager for Laravel
Installs: 233
Dependents: 1
Suggesters: 1
Security: 0
Stars: 0
Watchers: 2
Forks: 0
Open Issues: 0
pkg:composer/codewiser/simple-files
Requires
- php: ^8.1
- laravel/framework: >=10.0
- league/flysystem: ^3.0
Requires (Dev)
- phpunit/phpunit: ^10.0
README
No database, only local filesystem.
Every model keeps its files isolated from each over. Path to files formed from the model's morph name and its primary key.
Remember to
enforceMorphMapinAppServiceProvider!
Define storage
Implement Attachmentable contract on Model.
In the example below the model will keep files on default disk at post/{id}
path.
use Codewiser\Storage\Attachmentable; use Codewiser\Storage\Storage; use Codewiser\Storage\StorageContract; use Illuminate\Database\Eloquent\Model; class Post extends Model implements Attachmentable { public function storage(string|\BackedEnum $bucket = null): StorageContract { return Storage::make($this, bucket: $bucket); } }
Uploading files
You may add files from UploadedFile, from a local path or remote url.
You may upload multiple files at once.
use Illuminate\Http\Request; class Controller { public function attach(Request $request, Post $post) { return $post->storage() ->upload($request->allFiles()) ->toArray(); } }
[{
"path": "post/1/test.png",
"url": "/storage/post/1/test.png",
"name": "test.png",
"size": 6434,
"hash": "d41d8cd98f00b204e9800998ecf8427e",
"mime_type": "image/png",
"last_modified": "2025-02-18T12:29:46+00:00"
}]
Removing files
File path attribute is a relative path to a disk, e.g. post/1/test.png.
Use path attribute to delete a file. You may delete multiple files at once.
use Illuminate\Http\Request; class Controller { public function detach(Request $request, Post $post) { $post->storage() ->delete($request->input('unlink')); return response()->noContent(); } }
To remove all files call flush method on Storage:
$files = $post->storage()->flush();
List files
To get collection with all files call files method on Storage:
$files = $post->storage()->files(); return $files->toArray();
Storage object is Arrayable too. It returns the same:
$post->storage()->toArray(); // Is equivalent to $post->storage()->files()->toArray();
File object
File object has the same methods as Laravel Storage Facade: exists, size,
lastModified, delete, checksum, url etc.
Every stored file represented with such an array:
{
"path": "post/1/test.png",
"url": "/storage/post/1/test.png",
"name": "test.png",
"size": 6434,
"hash": "d41d8cd98f00b204e9800998ecf8427e",
"mime_type": "image/png",
"last_modified": "2025-02-18T12:29:46+00:00"
}
File object implements Responsable and Attachable, so you may use it as
Response and in Notification or Mailable.
Singular Storage
Sometimes we need the model to have only one file. We may create such a storage:
use Codewiser\Storage\Attachmentable; use Codewiser\Storage\Storage; use Codewiser\Storage\StorageContract; use Codewiser\Storage\Singular; use Illuminate\Database\Eloquent\Model; class Post extends Model implements Attachmentable { public function storage(string|\BackedEnum $bucket = null): StorageContract|Singular { return Storage::make($this, disk: 'public', bucket: $bucket)->singular(); } }
When you upload the next file to a storage, all previous files will be removed.
Singular storage has only one file, so files collection will contain only one
element maximum. You may use file method instead.
$post->storage()->toArray(); // Is equivalent to $post->storage()->file()->toArray();
Storage Pool
The model may have few storages at the same time. Storages must have unique names (aka buckets).
use Codewiser\Storage\Attachmentable; use Codewiser\Storage\Storage; use Codewiser\Storage\Singular; use Codewiser\Storage\StorageContract; use Illuminate\Database\Eloquent\Model; class Post extends Model implements Attachmentable { public function storage(string|\BackedEnum $bucket = null): StorageContract|Singular { return match ($bucket) // One cover 'cover' => Storage::make($this, bucket: $bucket) ->singular(), // Many docs 'docs' => Storage::make($this, bucket: $bucket), default => throw new \InvalidArgumentException("Bucket is not supported"), }; } }
Then we may get the exact bucket:
$docs = $post->storage('docs')->files(); $cover = $post->storage('cover')->file();
Default storage
It is allowed to have one default storage in a pool:
use Codewiser\Storage\Attachmentable; use Codewiser\Storage\Storage; use Codewiser\Storage\Singular; use Codewiser\Storage\StorageContract; use Illuminate\Database\Eloquent\Model; class Post extends Model implements Attachmentable { public function storage(string|\BackedEnum $bucket = null): StorageContract|Singular { return match ($bucket) // Named bucket 'docs' => Storage::make($this, bucket: $bucket), // Default bucket null => Storage::make($this)->singular(), default => throw new \InvalidArgumentException("Bucket is not supported"), }; } }
Call storage without bucket name to get the default one.
$cover = $post->storage()->file(); $docs = $post->storage('docs')->files();
Pool response
You may add a method to a model, that will return Pool object with all
buckets defined:
use Codewiser\Storage\Attachmentable; use Codewiser\Storage\Pool; use Codewiser\Storage\Storage; use Codewiser\Storage\Singular; use Codewiser\Storage\StorageContract; use Illuminate\Database\Eloquent\Model; class Post extends Model implements Attachmentable { public function pool(): Pool { return Pool::make() ->addBucket(Storage::make($this)->singular()) ->addBucket(Storage::make($this, bucket: 'docs')); } public function storage(string|\BackedEnum $bucket = null): StorageContract|Singular { return $this->pool()->getBucket($bucket); } }
Then you may use this method in api resource:
use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; class PostResource extends JsonResource { public function toArray(Request $request): array { return [ ...parent::toArray($request), 'files' => $this->pool()->toArray() ]; } }
Pool toArray method will return an array with every bucket and its file(s).
Singular storage provides file attribute, that may be null
if no file were uploaded. Base storage provides files array, that may be
empty.
[
{
"bucket": null,
"file": {
"path": "post/1/test.png",
"url": "/storage/post/1/test.png",
"name": "test.png",
"size": 6434,
"hash": "d41d8cd98f00b204e9800998ecf8427e",
"mime_type": "image/png",
"last_modified": "2025-02-18T12:29:46+00:00"
}
},
{
"bucket": "docs",
"files": [
{
"path": "post/1/docs/test.png",
"url": "/storage/post/1/docs/test.png",
"name": "test.png",
"size": 6434,
"hash": "d41d8cd98f00b204e9800998ecf8427e",
"mime_type": "image/png",
"last_modified": "2025-02-18T12:29:46+00:00"
}
]
}
]
Downloading files
The file is directly accessible only then published in public local filesystem. In other cases — private or cloud filesystem — application needs a controller to make files accessible to the users.
Let's say we have such a private disk in config/filesystems.php:
'local' => [ 'driver' => 'local', 'root' => storage_path('app/private'), 'url' => env('APP_URL').'/private', 'serve' => true, 'throw' => false, 'report' => false, ],
If so, file url would be about private/post/1/test.png (for default bucket)
or private/post/1/bucket/test.png (for named bucket).
We suggest to use a controller \Codewiser\Storage\StorageController, that
is looks so:
use Codewiser\Storage\File; use Codewiser\Storage\Storage; use Illuminate\Contracts\Support\Responsable; use Illuminate\Http\Request; use Illuminate\Support\Facades\Gate; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; class StorageController { public function __invoke(Request $request, string $model, string $id, string $bucket, string $filename = null): Responsable { if (is_null($filename)) { $filename = $bucket; $bucket = null; } $storage = Storage::resolve($model, $id, $bucket); Gate::authorize('view', $storage->owner()); return $storage->files()->sole( fn(File $file) => $file->filename() == $filename ); } }
All you need is to declare a route:
use Codewiser\Storage\StorageController; use Illuminate\Support\Facades\Route; Route::get('private/{model}/{id}/{bucket}/{filename?}', StorageController::class);