From 31c49ac3fc7c9576f6cd51ed07da85d7e25e0140 Mon Sep 17 00:00:00 2001 From: Jonny Barnes Date: Sat, 1 Aug 2026 17:07:38 +0100 Subject: [PATCH] Adopt Laravel's Image facade for media processing, upgrade Intervention to v4 Laravel 13's Image facade wraps Intervention Image v4 internally, so switching our upload width probe and resize job/command to it required bumping intervention/image ^3 -> ^4 (and its intervention/gif ^5 dependency). Removes our own ImageManager container binding and config/image.php in favour of Laravel's built-in driver resolution. Also fixes a latent filename mismatch in ProcessMediaJobTest that Pint's stricter typing on the new Image API turned into a hard TypeError, and tidies config/flare.php to use imported class names instead of FQCNs. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018Sorsgn85nw7uQyRMNvzyD --- app/Console/Commands/ReprocessMediaImages.php | 19 ++++----- .../Controllers/MicropubMediaController.php | 10 ++--- app/Jobs/ProcessMedia.php | 20 +++++----- app/Providers/AppServiceProvider.php | 6 --- composer.json | 2 +- composer.lock | 40 +++++++++---------- config/flare.php | 10 +++-- config/image.php | 22 ---------- tests/Unit/Jobs/ProcessMediaJobTest.php | 14 +++---- 9 files changed, 54 insertions(+), 89 deletions(-) delete mode 100644 config/image.php diff --git a/app/Console/Commands/ReprocessMediaImages.php b/app/Console/Commands/ReprocessMediaImages.php index c5e22d1b..b6c86c47 100644 --- a/app/Console/Commands/ReprocessMediaImages.php +++ b/app/Console/Commands/ReprocessMediaImages.php @@ -6,9 +6,9 @@ namespace App\Console\Commands; use App\Models\Media; use Illuminate\Console\Command; +use Illuminate\Image\ImageException; +use Illuminate\Support\Facades\Image; use Illuminate\Support\Facades\Storage; -use Intervention\Image\Exceptions\DecoderException; -use Intervention\Image\ImageManager; class ReprocessMediaImages extends Command { @@ -16,7 +16,7 @@ class ReprocessMediaImages extends Command protected $description = 'Regenerate medium and small image variants using correct aspect-ratio scaling'; - public function handle(ImageManager $manager): void + public function handle(): void { $media = Media::where('type', 'image') ->whereNotNull('image_widths') @@ -44,10 +44,10 @@ class ReprocessMediaImages extends Command $this->info("Processing: {$path}"); + $image = Image::fromStorage($path, 'public'); try { - $file = Storage::disk('public')->get($path); - $image = $manager->read($file); - } catch (DecoderException) { + $image->width(); + } catch (ImageException) { $this->warn(' Could not decode image, skipping.'); continue; @@ -57,11 +57,8 @@ class ReprocessMediaImages extends Command $extension = array_pop($filenameParts); $basename = trim(implode('.', $filenameParts), '.'); - $medium = $image->scale(width: 1000); - Storage::disk('public')->put($basename.'-medium.'.$extension, (string) $medium->encode()); - - $small = $image->scale(width: 500); - Storage::disk('public')->put($basename.'-small.'.$extension, (string) $small->encode()); + Storage::disk('public')->put($basename.'-medium.'.$extension, $image->scale(width: 1000)->toBytes()); + Storage::disk('public')->put($basename.'-small.'.$extension, $image->scale(width: 500)->toBytes()); $this->info(' Done.'); } diff --git a/app/Http/Controllers/MicropubMediaController.php b/app/Http/Controllers/MicropubMediaController.php index 1cca74b3..da7c7dc2 100644 --- a/app/Http/Controllers/MicropubMediaController.php +++ b/app/Http/Controllers/MicropubMediaController.php @@ -13,9 +13,10 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Http\UploadedFile; +use Illuminate\Image\ImageException; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Image; use Illuminate\Support\Facades\Storage; -use Intervention\Image\ImageManager; use Ramsey\Uuid\Uuid; class MicropubMediaController extends Controller @@ -111,12 +112,9 @@ class MicropubMediaController extends Controller $filename = Storage::disk('local')->putFile('media', $file); - /** @var ImageManager $manager */ - $manager = resolve(ImageManager::class); try { - $image = $manager->read($request->file('file')); - $width = $image->width(); - } catch (Exception) { + $width = Image::fromUpload($request->file('file'))->width(); + } catch (ImageException) { // not an image $width = null; } diff --git a/app/Jobs/ProcessMedia.php b/app/Jobs/ProcessMedia.php index f9d8af50..78aeba3e 100644 --- a/app/Jobs/ProcessMedia.php +++ b/app/Jobs/ProcessMedia.php @@ -7,11 +7,11 @@ namespace App\Jobs; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Image\ImageException; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Image; use Illuminate\Support\Facades\Storage; -use Intervention\Image\Exceptions\DecoderException; -use Intervention\Image\ImageManager; class ProcessMedia implements ShouldQueue { @@ -30,15 +30,16 @@ class ProcessMedia implements ShouldQueue /** * Execute the job. */ - public function handle(ImageManager $manager): void + public function handle(): void { // Load file $file = Storage::disk('local')->get($this->filename); // Open file + $image = Image::fromStorage($this->filename, 'local'); try { - $image = $manager->read($file); - } catch (DecoderException) { + $width = $image->width(); + } catch (ImageException) { // not an image; delete file and end job Storage::disk('local')->delete($this->filename); @@ -49,18 +50,15 @@ class ProcessMedia implements ShouldQueue Storage::disk('public')->put($this->filename, $file); // Create smaller versions if necessary - if ($image->width() > 1000) { + if ($width > 1000) { $filenameParts = explode('.', $this->filename); $extension = array_pop($filenameParts); // the following achieves this data flow // foo.bar.png => ['foo', 'bar', 'png'] => ['foo', 'bar'] => foo.bar $basename = trim(implode('.', $filenameParts), '.'); - $medium = $image->scale(width: 1000); - Storage::disk('public')->put($basename.'-medium.'.$extension, (string) $medium->encode()); - - $small = $image->scale(width: 500); - Storage::disk('public')->put($basename.'-small.'.$extension, (string) $small->encode()); + Storage::disk('public')->put($basename.'-medium.'.$extension, $image->scale(width: 1000)->toBytes()); + Storage::disk('public')->put($basename.'-small.'.$extension, $image->scale(width: 500)->toBytes()); } // Now we can delete the locally saved image diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 7718d7ec..224472d1 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -7,7 +7,6 @@ use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; use Illuminate\Support\Facades\URL; use Illuminate\Support\ServiceProvider; -use Intervention\Image\ImageManager; use Lcobucci\JWT\Configuration; use Lcobucci\JWT\Signer\Hmac\Sha256; use Lcobucci\JWT\Signer\Key\InMemory; @@ -30,11 +29,6 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { - // configure Intervention/Image - $this->app->bind('Intervention\Image\ImageManager', function () { - return ImageManager::withDriver(config('image.driver')); - }); - /** * Paginate a standard Laravel Collection. * diff --git a/composer.json b/composer.json index 7e55bbd3..520d12e1 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "ext-sodium": "*", "cviebrock/eloquent-sluggable": "^13.0", "indieauth/client": "^1.1", - "intervention/image": "^3", + "intervention/image": "^4.0", "jonnybarnes/indieweb": "~0.2", "jonnybarnes/webmentions-parser": "~0.5", "laravel/framework": "^13.0", diff --git a/composer.lock b/composer.lock index be951f93..6af58a17 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "01ba04ab77c167a38ed826d7193ba5ef", + "content-hash": "23983a4e6a8e79cb9636fe8f0e604eb7", "packages": [ { "name": "aws/aws-crt-php", @@ -1556,26 +1556,26 @@ }, { "name": "intervention/gif", - "version": "4.2.4", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/Intervention/gif.git", - "reference": "c3598a16ebe7690cd55640c44144a9df383ea73c" + "reference": "bb395af960deffe64d70c976b4df9283f68e762d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Intervention/gif/zipball/c3598a16ebe7690cd55640c44144a9df383ea73c", - "reference": "c3598a16ebe7690cd55640c44144a9df383ea73c", + "url": "https://api.github.com/repos/Intervention/gif/zipball/bb395af960deffe64d70c976b4df9283f68e762d", + "reference": "bb395af960deffe64d70c976b4df9283f68e762d", "shasum": "" }, "require": { - "php": "^8.1" + "php": "^8.3" }, "require-dev": { "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "phpunit/phpunit": "^12.0", "slevomat/coding-standard": "~8.0", - "squizlabs/php_codesniffer": "^3.8" + "squizlabs/php_codesniffer": "^4" }, "type": "library", "autoload": { @@ -1594,7 +1594,7 @@ "homepage": "https://intervention.io/" } ], - "description": "Native PHP GIF Encoder/Decoder", + "description": "PHP GIF Encoder/Decoder", "homepage": "https://github.com/intervention/gif", "keywords": [ "animation", @@ -1604,7 +1604,7 @@ ], "support": { "issues": "https://github.com/Intervention/gif/issues", - "source": "https://github.com/Intervention/gif/tree/4.2.4" + "source": "https://github.com/Intervention/gif/tree/5.0.1" }, "funding": [ { @@ -1620,31 +1620,31 @@ "type": "ko_fi" } ], - "time": "2026-01-04T09:27:23+00:00" + "time": "2026-05-03T06:04:47+00:00" }, { "name": "intervention/image", - "version": "3.11.8", + "version": "4.2.0", "source": { "type": "git", "url": "https://github.com/Intervention/image.git", - "reference": "cf04c8dd245697f701057c13d4bfe140d584e738" + "reference": "830907fc5397dfc2a51a4e90322d586989fc8364" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Intervention/image/zipball/cf04c8dd245697f701057c13d4bfe140d584e738", - "reference": "cf04c8dd245697f701057c13d4bfe140d584e738", + "url": "https://api.github.com/repos/Intervention/image/zipball/830907fc5397dfc2a51a4e90322d586989fc8364", + "reference": "830907fc5397dfc2a51a4e90322d586989fc8364", "shasum": "" }, "require": { "ext-mbstring": "*", - "intervention/gif": "^4.2", - "php": "^8.1" + "intervention/gif": "^5", + "php": "^8.3" }, "require-dev": { "mockery/mockery": "^1.6", "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "phpunit/phpunit": "^12.0", "slevomat/coding-standard": "~8.0", "squizlabs/php_codesniffer": "^4" }, @@ -1680,7 +1680,7 @@ ], "support": { "issues": "https://github.com/Intervention/image/issues", - "source": "https://github.com/Intervention/image/tree/3.11.8" + "source": "https://github.com/Intervention/image/tree/4.2.0" }, "funding": [ { @@ -1696,7 +1696,7 @@ "type": "ko_fi" } ], - "time": "2026-05-01T08:20:10+00:00" + "time": "2026-07-09T13:07:14+00:00" }, { "name": "jonnybarnes/indieweb", diff --git a/config/flare.php b/config/flare.php index bb9cbd4e..0b8c6187 100644 --- a/config/flare.php +++ b/config/flare.php @@ -1,5 +1,9 @@ \Spatie\LaravelFlare\FlareConfig::defaultCollects( + 'collects' => FlareConfig::defaultCollects( ignore: [], extra: [] ), @@ -74,7 +78,7 @@ return [ */ 'sender' => [ - 'class' => \Spatie\LaravelFlare\Senders\LaravelHttpSender::class, + 'class' => LaravelHttpSender::class, 'config' => [ 'timeout' => 10, ], @@ -165,7 +169,7 @@ return [ */ 'sampler' => [ - 'class' => \Spatie\FlareClient\Sampling\RateSampler::class, + 'class' => RateSampler::class, 'config' => [ 'rate' => env('FLARE_SAMPLER_RATE', 0.1), ], diff --git a/config/image.php b/config/image.php deleted file mode 100644 index b984a0f0..00000000 --- a/config/image.php +++ /dev/null @@ -1,22 +0,0 @@ - Driver::class, - -]; diff --git a/tests/Unit/Jobs/ProcessMediaJobTest.php b/tests/Unit/Jobs/ProcessMediaJobTest.php index da9f070c..9ffa2eee 100644 --- a/tests/Unit/Jobs/ProcessMediaJobTest.php +++ b/tests/Unit/Jobs/ProcessMediaJobTest.php @@ -6,7 +6,6 @@ namespace Tests\Unit\Jobs; use App\Jobs\ProcessMedia; use Illuminate\Support\Facades\Storage; -use Intervention\Image\ImageManager; use PHPUnit\Framework\Attributes\Test; use Tests\TestCase; @@ -15,10 +14,9 @@ class ProcessMediaJobTest extends TestCase #[Test] public function non_media_files_are_not_saved(): void { - $manager = app()->make(ImageManager::class); Storage::disk('local')->put('media/file.txt', 'This is not an image'); - $job = new ProcessMedia('file.txt'); - $job->handle($manager); + $job = new ProcessMedia('media/file.txt'); + $job->handle(); $this->assertFileDoesNotExist(storage_path('app/media/').'file.txt'); } @@ -26,10 +24,9 @@ class ProcessMediaJobTest extends TestCase #[Test] public function small_images_are_not_resized(): void { - $manager = app()->make(ImageManager::class); Storage::disk('local')->put('media/aaron.png', file_get_contents(__DIR__.'/../../aaron.png')); - $job = new ProcessMedia('aaron.png'); - $job->handle($manager); + $job = new ProcessMedia('media/aaron.png'); + $job->handle(); $this->assertFileDoesNotExist(storage_path('app/media/').'aaron.png'); @@ -41,10 +38,9 @@ class ProcessMediaJobTest extends TestCase #[Test] public function large_images_have_smaller_images_created(): void { - $manager = app()->make(ImageManager::class); Storage::disk('local')->put('media/test-image.jpg', file_get_contents(__DIR__.'/../../test-image.jpg')); $job = new ProcessMedia('media/test-image.jpg'); - $job->handle($manager); + $job->handle(); // These need to look in public disk Storage::disk('public')->assertExists('media/test-image.jpg');