codesleeve/laravel-stapler
Laravel Stapler is an easy file-attachment package for Laravel that handles uploads, storage, and image processing with a simple configuration. It supports local and S3 storage, automatic resizing and thumbnails, and clean model integration.
Stapler binds file uploads to Eloquent models Paperclip-style: you declare an attachment, and the package handles write, transform, URL, and delete. The mental model is "the file is a column on the row," so start by thinking in those terms.
Install and wire up
Check the composer constraint resolves against your framework first — this package targets the Laravel 4.2–5.x era and will not install on Laravel 9+ without a fork.
composer require codesleeve/laravel-stapler
Register the provider explicitly (auto-discovery is unreliable here):
// config/app.php
Codesleeve\LaravelStapler\LaravelStaplerServiceProvider::class,
Publish config and read it before writing model code — this is your first stop:
php artisan vendor:publish --provider="Codesleeve\LaravelStapler\LaravelStaplerServiceProvider"
config/stapler.php holds default storage (filesystem vs s3), path/URL interpolations, and the Imagine driver (gd/imagick). Every quirk you'll hit later traces back to this file.
First use case — a single avatar
The smallest working slice: a User with one avatar, two styles.
use Codesleeve\Stapler\ORM\StaplerableInterface;
use Codesleeve\Stapler\ORM\EloquentTrait;
class User extends Eloquent implements StaplerableInterface
{
use EloquentTrait;
protected $fillable = ['avatar'];
public function __construct(array $attributes = [])
{
$this->hasAttachedFile('avatar', [
'styles' => [
'thumb' => '64x64#',
'medium' => '256x256',
],
]);
parent::__construct($attributes);
}
}
Migration adds the fields Stapler expects:
$table->string('avatar_file_name');
$table->string('avatar_content_type');
$table->integer('avatar_file_size');
$table->timestamp('avatar_updated_at');
Assign via the request, save the model, read the URL:
$user->avatar = Input::file('avatar'); // Symfony UploadedFile
$user->save();
$user->avatar->url('thumb'); // string URL
$user->avatar->path('medium'); // absolute path on disk
If that round-trips (upload → thumbnail appears on disk → URL resolves), the package is wired. If it doesn't, the failure is almost always storage paths in config/stapler.php or the imagine driver missing — check those before anything else.
Declaring attachments in the constructor
The hasAttachedFile() calls belong in the model constructor (after parent), not in boot(). The trait relies on the properties being set at instantiation. A very common bug is putting them in boot() and wondering why styles never register.
Styles — the core feature
Styles are named derivatives defined inline. The syntax is Imagine geometry with an operator:
'styles' => [
'original' => '600x600>', // shrink, preserve ratio
'thumb' => '100x100#', // resize and center-crop
'large' => '1024x1024',
],
> shrink only, < grow only, ! force, # center-crop.original to keep the untouched upload as the base and generate only derivatives.original style is always stored; derivative styles are generated synchronously at save time.Interpolation strings — where files land
Paths and URLs are template strings using model attributes. This is the knob you tune per model:
$this->hasAttachedFile('photo', [
'url' => '/system/:attachment/:id_partition/:style/:filename',
'path' => ':laravel_root/public/system/:attachment/:id_partition/:style/:filename',
'styles' => [ 'thumb' => '80x80#' ],
]);
:id_partition groups IDs into 000/000/001 subpaths — keep it, because putting thousands of files in one directory degrades on some filesystems and slows listing. :hash avoids filename collisions when users upload same-named files. :style is mandatory for URL templates with multiple styles.
Validation
Stapler does not validate by itself. Validate in the FormRequest or controller before assigning:
$rules = ['avatar' => 'required|image|max:5120|mimes:jpg,jpeg,png,gif'];
Then attach the file. A missing validation layer means arbitrary UploadedFile shapes hit Imagine — treat this as a hard requirement, not optional polish.
Storage: local vs S3
Swap the default in config/stapler.php, or override per-attachment:
'storage' => 's3',
's3_client_config' => [
'key' => env('AWS_KEY'),
'secret' => env('AWS_SECRET'),
'region' => 'us-east-1',
'bucket' => env('AWS_BUCKET'),
],
S3 URL templates look different — usually :s3_object_url. Remember: on S3, every byte transits your PHP worker. There is no direct upload.
Automatic cleanup
Deleting the model deletes file(s) via the deleting event, as long as use EloquentTrait is intact on the model. Soft deletes do not trigger cleanup — by design, a soft delete leaves the file on disk. Handle that explicitly in whatever retention flow you have.
Replacing an attachment
Assign a new UploadedFile and save. Stapler deletes the old derivative set and writes new ones. There's no version history — if you need it, that's a custom implementation on top.
Polymorphic attachments
For a shared "attachment" table across models, use Stapler on the child record (one attachment per row) and let Eloquent's polymorphic relations do the rest. Stapler's own multi-attachment support is limited — it's not a media library. If your requirements drift toward galleries or multi-file records, treat that as a signal to evaluate a maintained alternative.
Extension points
Codesleeve\Stapler\Storage\StorageInterface.::filename interpolation or set 'filename' => function($file) { ... }.Wrap any custom logic behind an internal AttachmentService interface. The public API is small enough that a thin service in front of it makes a future replace-stapler migration an afternoon instead of a sprint.
Version constraints are the whole game. Laravel 9+/PHP 8.1+ will not resolve. If composer require fails, the evaluation is over — don't burn a day fighting constraints, fork and patch, or pivot. This is a go/no-go gate, not a step to work around.
Auto-discovery is unreliable. Always register the provider manually in config/app.php. If you see Class 'Codesleeve\LaravelStapler\...' not found at boot, this is why.
Never trust filenames. Path traversal and accidental overwrites are live risks if interpolation includes the raw client filename. Use :hash or override the filename callback with a sanitizer (basename(), allowlist characters, strip null bytes). The package will happily write wherever you tell it — that's a feature and a footgun.
Synchronous processing is the default. Large uploads → long request times → PHP memory exhaustion. Practical mitigations on the current package: cap upload size aggressively in validation, tune memory_limit and max_execution_time, and consider an out-of-band conversion job that shells into Imagine against the stored original, since Stapler won't queue for you.
Silent partial failures. It's common for one style to fail (missing directory, Imagine decode error) while the rest persist. Nothing throws to the user; the result is broken thumbnails rather than an exception. After a big deploy or config change, spot-check path('thumb') for a few fresh records.
Orphaned files are a real class of bug. Rollback after file write, or delete-record-after-save failure, leaves files on disk. There is no reconciliation job. Build one: nightly diff of storage/app/system directory vs the model table, report or delete orphans beyond a grace window.
Concurrent derivatization races. Two simultaneous saves for the same new model (rare but real) can collide on the same derivative paths. There's no locking. If your domain exposes this, add a lock at the service layer.
Config caching bites on class callbacks. If config/stapler.php uses PHP closures for filenames or interpolations, php artisan config:cache will break them (closures aren't serializable). Use string callbacks ('My\\FileNameResolver::build') or move logic into the model.
Path vs URL mismatch is the #1 debug rabbit hole. path() returns a filesystem path; url() returns the public URL. If a file generates but the image is broken, compare realpath($model->photo->path('thumb')) against where your web server serves $model->photo->url('thumb'). Nine times out of ten the path or url interpolation is wrong, not the file.
S3 legacy client. The bundled adapter uses AWS SDK v2. S3 SigV4, newer regions, and anything requiring modern SDK semantics need a fork. Verify your target region and bucket policy before assuming S3 works out of the box.
Imagick vs GD matters for output quality and edge cases. Set the driver explicitly in config/stapler.php. GD is more available; Imagick handles formats and colors better. Test thumbnail quality with the actual production driver, not the dev one.
No queue, no direct-to-S3, no resumable uploads. If any of those are on the roadmap, choose a maintained alternative now rather than discovering it mid-project. spatie/laravel-medialibrary is the default forward path; a thin Flysystem + Intervention Image service is the fully-owned option.
Pin the version and name an owner. The package is unmaintained, and you inherit its security surface (MIME validation, image decode limits, public-disk exposure). The mitigation is organizational, not technical: pin, isolate behind an internal interface, and put a dated exit plan in the codebase.
How can I help you explore Laravel packages today?