andrewelkins/cabinet
Cabinet is a Laravel package for managing file uploads and attachments. It lets you associate uploaded files with Eloquent models, handle storage and retrieval, and add file management to your app through a simple, expressive API.
Cabinet is a thin wrapper around Flysystem that gives Laravel 4 a single-file-upload API with a local or S3 backend. It is a legacy package (last release Feb 2015, Laravel 4 only) — treat this section as instructions for maintaining or extending an existing L4 app, not for new projects.
1. Install
// composer.json
"require": {
"andrewelkins/cabinet": "~1.0"
}
composer update andrewelkins/cabinet
2. Register the service provider and alias in app/config/app.php:
'providers' => [
// ...
'AndrewElkins\Cabinet\CabinetServiceProvider',
],
'aliases' => [
// ...
'Cabinet' => 'AndrewElkins\Cabinet\Facades\Cabinet',
],
3. Publish and edit config
php artisan config:publish andrewelkins/cabinet
That writes to app/config/packages/andrewelkins/cabinet/config.php. Set your driver and paths:
return [
'driver' => 'local', // or 's3'
'path' => storage_path('uploads'),
's3' => [
'key' => getenv('AWS_KEY'),
'secret' => getenv('AWS_SECRET'),
'bucket' => getenv('AWS_BUCKET'),
'region' => 'us-east-1',
],
];
4. Where to look first
app/config/packages/andrewelkins/cabinet/config.php — driver, path, S3 credentials, and any visibility options.vendor/andrewelkins/cabinet/src/ — the driver classes. This is the source of truth if the README lags behind your pinned version; method names (put, get, remove, url) are small enough to confirm in one file.5. First successful upload
The package's surface is intentionally small: put() returns an opaque file id, get() returns a file object, remove() deletes, url() produces a public URL for the local/S3 backend.
// app/controllers/UploadController.php
public function store()
{
$file = Input::file('upload');
if (! $file || ! $file->isValid()) {
return Redirect::back()->withErrors('Upload failed.');
}
$id = Cabinet::put($file); // persist this id
return Redirect::to('uploads')->with('file_id', $id);
}
public function show($id)
{
return Response::make(Cabinet::get($id)->getContent())
->header('Content-Type', Cabinet::get($id)->getMimeType());
}
Store the returned id on your model (e.g. avatar_id), not the original filename — Cabinet generates the storage key itself.
Attach uploads to an Eloquent model. Keep a file_id (or avatar_id, attachment_id) column and wrap retrieval in an accessor so views never touch the facade directly:
class User extends Eloquent
{
public function getAvatarAttribute()
{
return $this->avatar_id ? Cabinet::get($this->avatar_id) : null;
}
public function getAvatarUrlAttribute()
{
return $this->avatar_id ? Cabinet::url($this->avatar_id) : '/img/default.png';
}
}
Validate before you store. Cabinet does not validate. Do it in the controller or a form request:
$rules = ['upload' => 'required|file|max:5120|mimes:jpg,jpeg,png,pdf'];
$v = Validator::make(Input::all(), $rules);
if ($v->fails()) { /* bail before Cabinet::put() */ }
Serve private files through a route. With the local driver, Cabinet::url() only works if path is inside public/. For anything user-private, keep path under storage/ and stream through a controller that does its own Auth::check() and ownership check before calling Cabinet::get($id).
Swap drivers per environment. Because the config is environment-aware, use local in app/config/local/packages/andrewelkins/cabinet/config.php and s3 in app/config/production/.... Both drivers expose the same facade, so controller code does not change. This is the single biggest reason to use Cabinet over ad-hoc move_uploaded_file calls.
Post-process images before storing. Cabinet doesn't transform files. If you need thumbnails, read the file, run Intervention, then store the derived versions yourself:
$image = Image::make($file->getRealPath())->resize(200, null, function ($c) { $c->aspectRatio(); });
$thumbId = Cabinet::put($image->encode('jpg'));
Keep a small table (uploads with original_id, thumb_id, mime, bytes, original_name) so you can track derived assets and know what to delete.
Clean up on delete. Hook model events so orphaned blobs don't accumulate:
User::deleting(function ($user) {
if ($user->avatar_id) { Cabinet::remove($user->avatar_id); }
});
Test against a temp directory. Point the config path at storage_path('testing/uploads') in your test bootstrap and File::cleanDirectory() in setUp(). There is no Storage::fake() in L4, so this is the closest equivalent.
One facade call per request where possible. Cabinet::get($id) returns an in-memory file object; call it once, assign to a variable, and reuse it for getContent(), getMimeType(), and getSize().
Storage facade (L5+) instead — the API is a superset of what Cabinet provides. Only adopt Cabinet if you're already pinned to an L4 codebase.enctype="multipart/form-data". A missing enctype produces null from Input::file() with no error; the most common "Cabinet is broken" report is actually a form-encoding bug.isValid() before put(). Symfony's UploadedFile::isValid() catches PHP upload errors (size, partial upload, missing tmp dir). Cabinet will happily throw on an invalid file otherwise.upload_max_filesize, post_max_size, and client_max_body_size all need raising for large files. When Input::file() is null but the form is correct, this is almost always the cause.Cabinet::url() is only meaningful for the local driver when path is web-accessible. Otherwise build a route that streams the file. Do not paper over this by publishing your storage/ directory.Cabinet::remove($id) in a catch block. In a DB transaction, do the file removal in an after-commit callback or accept a small orphan window and run a cleanup job.aws/aws-sdk-php. Adding any new AWS-backed package will often conflict. Run composer why aws/aws-sdk-php and consider dropping Cabinet's S3 driver in favour of a direct Storage/Flysystem call rather than upgrading the whole SDK.app/config/packages/..., not app/config/. The facade reads the namespaced config. Editing app/config/cabinet.php does nothing — a classic L4 footgun.storage/uploads (or your configured path) is writable by the web user. A permission error surfaces as a generic exception from Flysystem, not a friendly upload message.Cabinet::url() will otherwise hand back a URL that 403s. Public-read URLs are a common accidental data exposure — audit what user uploads end up in.App\Uploads\Storage) and call the facade from there. That also gives you one seam to swap out when you eventually migrate to L5's Storage.dd(Cabinet::get($id)) to confirm the file object resolves, then dd(Cabinet::url($id)) for the resolved path/URL. If get() returns null, the id is stale or the driver config is wrong — check app/config/packages/... under the active environment, not just the default file.How can I help you explore Laravel packages today?