Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Eloquent Dynamic Photos Laravel Package

behzadsp/eloquent-dynamic-photos

View on GitHub
Deep Wiki
Context7

Getting Started

Install the right release. composer require behzadsp/eloquent-dynamic-photos and confirm you land on v2.0.1, not v2.0.0. Packagist pinned v2.0.0 to a commit that predates the Laravel 11 drop, so it cannot be installed against the intended support matrix — if you see v2.0.0 in composer.lock, bump it.

Check your platform first. PHP ^8.3, Laravel ^12.0 || ^13.0, intervention/image ^4.2. If you are on Laravel 11 this package is not for you: the release deliberately excludes it (past its security window, seven unfixed framework advisories, and Composer 2.10+ blocks the 11.x line anyway).

Publish the config under the new tag — the file moved to config/eloquent_photo.php:

php artisan vendor:publish --tag=eloquent-photo-config
php artisan storage:link

Where to look first, in order:

  1. UPGRADE.md if you have any v1 code — the model-facing API changed completely, so there is no incremental path.
  2. config/eloquent_photo.php — note directory (was root_directory), name_limit (was slug_limit), quality (now 75), driver, auto_orient, delete_files_on_model_delete, queue_conversions.
  3. The PhotoDefinition, Conversion, and Fit classes themselves — they are the API surface now.
  4. The test suite (composer test) — 167 tests with real encoded fixtures are the de facto usage documentation.

First use case. Add a nullable string column for the photo, declare it once on the model, write it from a controller, render it in Blade:

// migration
$table->string('avatar')->nullable();

// model
public function photoDefinitions(): array
{
    return [
        'avatar' => PhotoDefinition::make()
            ->disk('public')
            ->directory('avatars')
            ->format('webp'),
    ];
}

// controller
$model->updatePhoto('avatar', $request->file('avatar'));

// blade
<img src="{{ $model->photoUrl('avatar') }}">

That single declaration replaces v1's seven per-model override methods.

Implementation Patterns

Declare per field, not per model. This is the headline change and the reason to adopt v2. Two photo columns on the same model can now diverge on disk, directory, format, and conversion set:

public function photoDefinitions(): array
{
    return [
        'avatar' => PhotoDefinition::make()
            ->disk('public')->directory('avatars')->format('webp'),
        'hero'   => PhotoDefinition::make()
            ->disk('s3')->directory('heroes')->format('jpg'),
    ];
}

Keep the declarations in one method on the model so the full photo surface is greppable.

Treat conversions as derived, not stored state. Declare named conversions on the field; they are generated from the stored primary during updatePhoto(). When you add a conversion to an existing definition, backfill with:

$model->regeneratePhotoConversions('avatar');

It is idempotent, so it is safe to run from a console command, a queue job, or a loop over chunkById() without guarding for "already done". This is the pattern for a rolling deploy that introduces a new thumbnail size.

Defer conversions, never the primary. For large images, enable queue_conversions and set queue.connection / queue.name appropriately. Only conversions go through ProcessConversionsJob; the primary is always written synchronously so the column is never pointing at nothing. Design your UI for the window where the primary exists and the thumbnail does not.

Pick your delete verb deliberately. Three distinct operations, three distinct meanings:

  • deletePhotoFiles($field) — files only, column untouched (renamed from v1's deletePhotoFile()).
  • deletePhoto($field) — files and clears/saves the column. New in v2, and usually what you want.
  • Model delete — automatic via the deleted hook, governed by delete_files_on_model_delete.

Lean on the lifecycle hook, but understand its boundaries. Cleanup respects soft deletes (files survive SoftDeletes::delete(), go on forceDelete()) and waits for the enclosing transaction to commit before touching disk. That makes it safe to delete a model inside a DB::transaction() without leaving orphaned files on rollback — a pattern that was painful to hand-roll in v1.

Validate at the edge, then let the processor enforce. PhotoProcessor already does a MIME allowlist check twice (container sniffed from the raw header pre-decode, then the decoded image), plus max_file_size, max_width/max_height, and max_pixels. Add your normal Laravel image/mimes rules for user-facing error messages, but treat the package checks as the authoritative guard — particularly for anything user-supplied.

Test your own integration with Storage::fake(). The package's own suite does exactly this, and it works cleanly here because file writes go through Storage::put() and raise StorageFailedException on false rather than failing silently. Assert on the primary path and each conversion path.

Gotchas and Tips

Do not install v2.0.0. It cannot be moved off the tag Packagist ingested and predates the Laravel 11 drop. Pin ^2.0.1 in composer.json.

There is no partial v1 upgrade. The override-method API is gone — no getEloquentPhotoQuality(), no getEloquentPhotoSlugLimit(), no getPhotoFullPath(). Work through UPGRADE.md; do not try to keep v1 idioms alive.

The path/URL methods changed signature. photoUrl(), photoPath(), and photoFullPath() now take a field name, not a raw path. v1's getPhotoFullPath($path) took the path itself — passing a path to v2's method will resolve a nonsense field.

Config keys were renamed, and every value is type-checked. root_directory → directory, slug_limit → name_limit. A stale published config silently loses those values, and a wrongly-typed value now raises PhotoDefinitionException naming the key instead of being cast. env()-shaped strings ("75", "true") are tolerated because Laravel's env() leaves them as strings — but write real typed values in the config file wherever you can, so the exception surfaces at boot rather than on first upload.

quality now defaults to 75, not 50. New uploads are bigger and better looking. Existing stored files are untouched — so a bulk re-encode is a deliberate migration decision, not something the upgrade does for you. Budget for storage growth if you have high upload volume.

updatePhoto() no longer wraps the save in a transaction. That is intentional (the file write is outside the transaction anyway), but it means you cannot rely on the package to roll back a save. The ordering is now write-new → save → conversions → delete-old, so a mid-flight failure leaves an orphaned file rather than a column pointing at a deleted one. Sweep orphans occasionally; they are harmless but accumulate.

Catch StorageFailedException. Laravel's default filesystem config returns false from Storage::put() on failure instead of throwing, so a disk misconfiguration or an S3 permission error otherwise looks like success. This covers both the primary and every conversion. Let it bubble as a 500 or convert it to a user-facing "upload failed" — but do not swallow it.

photoFullPath() is meaningless on remote disks. v1's version silently returned a locally-prefixed key that was not a real path; v2 inspects the disk properly. If you are on S3, use photoUrl()/photoPath() and never photoFullPath() for local file operations.

Conversion storage is per-primary now. Files live at {directory}/{model-dir}/conversions/{primary-basename}/{conversion}.{format} — one directory per primary holding the whole set. Useful side effect: removing a primary's conversions is a single directory delete rather than iterating declared names. Do not hand-manage these paths; the layout is an implementation detail you should treat as opaque.

max_width/max_height downscale, they never reject and never upscale. If you want an upload rejected for being too large, that is what max_file_size is for. max_pixels is a different animal: it is a pre-decode memory guard that accounts for GIF and WebP frame counts, sized so a hostile container never reaches the decoder. The documented measurement — a 1.4 MB 100-page LZW TIFF going from ~3 GB allocated to 0 MB under imagick — is the reason to set it low and leave it set.

auto_orient needs ext-exif and is a silent no-op without it. If rotated phone photos are coming out sideways in production but fine locally, check the extension before you check the config.

The _url accessor is now gated on photoDefinitions(). It only fires for declared fields, and it strips exactly one trailing _url via substr(). This means real columns like website/website_url fall through to normal attribute resolution correctly — but it also means a field you forgot to declare has no _url magic. Prefer calling photoUrl($field) explicitly in Blade; it reads better and fails loudly on a typo.

Choose the driver in config, not in your Dockerfile. driver (gd / imagick) resolves at runtime through Intervention v4's ImageManager::usingDriver(). There is no hard ext-gd dependency, so you can ship imagick in production and gd in CI — or the reverse — without touching application code.

Filenames are slugged. All path construction routes through Str::slug(), which closed a real traversal hole in v1 (/ and .. in the name attribute). If you were relying on a user-supplied filename surviving intact, it will not — and the Str::limit() ... terminator no longer leaks into long names either.

Extension points are the value objects. PhotoDefinition, Conversion, and Fit are the places to look if you need behaviour beyond config. Everything the package does per field flows through a definition, so a shared trait or a helper that builds common definitions keeps your models readable when the number of photo columns grows.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource