MTM Improve app by leveraging more Laravel code #111

Merged
jonny merged 4 commits from develop into main 2026-08-01 20:37:16 +02:00
9 changed files with 54 additions and 89 deletions
Showing only changes of commit 31c49ac3fc - Show all commits

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sorsgn85nw7uQyRMNvzyD
Jonny Barnes 2026-08-01 17:07:38 +01:00
Signed by: jonny
SSH key fingerprint: SHA256:CTuSlns5U7qlD9jqHvtnVmfYV3Zwl2Z7WnJ4/dqOaL8

View file

@ -6,9 +6,9 @@ namespace App\Console\Commands;
use App\Models\Media; use App\Models\Media;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Image\ImageException;
use Illuminate\Support\Facades\Image;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\ImageManager;
class ReprocessMediaImages extends Command 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'; 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') $media = Media::where('type', 'image')
->whereNotNull('image_widths') ->whereNotNull('image_widths')
@ -44,10 +44,10 @@ class ReprocessMediaImages extends Command
$this->info("Processing: {$path}"); $this->info("Processing: {$path}");
$image = Image::fromStorage($path, 'public');
try { try {
$file = Storage::disk('public')->get($path); $image->width();
$image = $manager->read($file); } catch (ImageException) {
} catch (DecoderException) {
$this->warn(' Could not decode image, skipping.'); $this->warn(' Could not decode image, skipping.');
continue; continue;
@ -57,11 +57,8 @@ class ReprocessMediaImages extends Command
$extension = array_pop($filenameParts); $extension = array_pop($filenameParts);
$basename = trim(implode('.', $filenameParts), '.'); $basename = trim(implode('.', $filenameParts), '.');
$medium = $image->scale(width: 1000); Storage::disk('public')->put($basename.'-medium.'.$extension, $image->scale(width: 1000)->toBytes());
Storage::disk('public')->put($basename.'-medium.'.$extension, (string) $medium->encode()); Storage::disk('public')->put($basename.'-small.'.$extension, $image->scale(width: 500)->toBytes());
$small = $image->scale(width: 500);
Storage::disk('public')->put($basename.'-small.'.$extension, (string) $small->encode());
$this->info(' Done.'); $this->info(' Done.');
} }

View file

@ -13,9 +13,10 @@ use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Image\ImageException;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Image;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Intervention\Image\ImageManager;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
class MicropubMediaController extends Controller class MicropubMediaController extends Controller
@ -111,12 +112,9 @@ class MicropubMediaController extends Controller
$filename = Storage::disk('local')->putFile('media', $file); $filename = Storage::disk('local')->putFile('media', $file);
/** @var ImageManager $manager */
$manager = resolve(ImageManager::class);
try { try {
$image = $manager->read($request->file('file')); $width = Image::fromUpload($request->file('file'))->width();
$width = $image->width(); } catch (ImageException) {
} catch (Exception) {
// not an image // not an image
$width = null; $width = null;
} }

View file

@ -7,11 +7,11 @@ namespace App\Jobs;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Image\ImageException;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Image;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\ImageManager;
class ProcessMedia implements ShouldQueue class ProcessMedia implements ShouldQueue
{ {
@ -30,15 +30,16 @@ class ProcessMedia implements ShouldQueue
/** /**
* Execute the job. * Execute the job.
*/ */
public function handle(ImageManager $manager): void public function handle(): void
{ {
// Load file // Load file
$file = Storage::disk('local')->get($this->filename); $file = Storage::disk('local')->get($this->filename);
// Open file // Open file
$image = Image::fromStorage($this->filename, 'local');
try { try {
$image = $manager->read($file); $width = $image->width();
} catch (DecoderException) { } catch (ImageException) {
// not an image; delete file and end job // not an image; delete file and end job
Storage::disk('local')->delete($this->filename); Storage::disk('local')->delete($this->filename);
@ -49,18 +50,15 @@ class ProcessMedia implements ShouldQueue
Storage::disk('public')->put($this->filename, $file); Storage::disk('public')->put($this->filename, $file);
// Create smaller versions if necessary // Create smaller versions if necessary
if ($image->width() > 1000) { if ($width > 1000) {
$filenameParts = explode('.', $this->filename); $filenameParts = explode('.', $this->filename);
$extension = array_pop($filenameParts); $extension = array_pop($filenameParts);
// the following achieves this data flow // the following achieves this data flow
// foo.bar.png => ['foo', 'bar', 'png'] => ['foo', 'bar'] => foo.bar // foo.bar.png => ['foo', 'bar', 'png'] => ['foo', 'bar'] => foo.bar
$basename = trim(implode('.', $filenameParts), '.'); $basename = trim(implode('.', $filenameParts), '.');
$medium = $image->scale(width: 1000); Storage::disk('public')->put($basename.'-medium.'.$extension, $image->scale(width: 1000)->toBytes());
Storage::disk('public')->put($basename.'-medium.'.$extension, (string) $medium->encode()); Storage::disk('public')->put($basename.'-small.'.$extension, $image->scale(width: 500)->toBytes());
$small = $image->scale(width: 500);
Storage::disk('public')->put($basename.'-small.'.$extension, (string) $small->encode());
} }
// Now we can delete the locally saved image // Now we can delete the locally saved image

View file

@ -7,7 +7,6 @@ use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\URL; use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Intervention\Image\ImageManager;
use Lcobucci\JWT\Configuration; use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Hmac\Sha256; use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Signer\Key\InMemory;
@ -30,11 +29,6 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot(): void 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. * Paginate a standard Laravel Collection.
* *

View file

@ -15,7 +15,7 @@
"ext-sodium": "*", "ext-sodium": "*",
"cviebrock/eloquent-sluggable": "^13.0", "cviebrock/eloquent-sluggable": "^13.0",
"indieauth/client": "^1.1", "indieauth/client": "^1.1",
"intervention/image": "^3", "intervention/image": "^4.0",
"jonnybarnes/indieweb": "~0.2", "jonnybarnes/indieweb": "~0.2",
"jonnybarnes/webmentions-parser": "~0.5", "jonnybarnes/webmentions-parser": "~0.5",
"laravel/framework": "^13.0", "laravel/framework": "^13.0",

40
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "01ba04ab77c167a38ed826d7193ba5ef", "content-hash": "23983a4e6a8e79cb9636fe8f0e604eb7",
"packages": [ "packages": [
{ {
"name": "aws/aws-crt-php", "name": "aws/aws-crt-php",
@ -1556,26 +1556,26 @@
}, },
{ {
"name": "intervention/gif", "name": "intervention/gif",
"version": "4.2.4", "version": "5.0.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Intervention/gif.git", "url": "https://github.com/Intervention/gif.git",
"reference": "c3598a16ebe7690cd55640c44144a9df383ea73c" "reference": "bb395af960deffe64d70c976b4df9283f68e762d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Intervention/gif/zipball/c3598a16ebe7690cd55640c44144a9df383ea73c", "url": "https://api.github.com/repos/Intervention/gif/zipball/bb395af960deffe64d70c976b4df9283f68e762d",
"reference": "c3598a16ebe7690cd55640c44144a9df383ea73c", "reference": "bb395af960deffe64d70c976b4df9283f68e762d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^8.1" "php": "^8.3"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "^2.1", "phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", "phpunit/phpunit": "^12.0",
"slevomat/coding-standard": "~8.0", "slevomat/coding-standard": "~8.0",
"squizlabs/php_codesniffer": "^3.8" "squizlabs/php_codesniffer": "^4"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@ -1594,7 +1594,7 @@
"homepage": "https://intervention.io/" "homepage": "https://intervention.io/"
} }
], ],
"description": "Native PHP GIF Encoder/Decoder", "description": "PHP GIF Encoder/Decoder",
"homepage": "https://github.com/intervention/gif", "homepage": "https://github.com/intervention/gif",
"keywords": [ "keywords": [
"animation", "animation",
@ -1604,7 +1604,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/Intervention/gif/issues", "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": [ "funding": [
{ {
@ -1620,31 +1620,31 @@
"type": "ko_fi" "type": "ko_fi"
} }
], ],
"time": "2026-01-04T09:27:23+00:00" "time": "2026-05-03T06:04:47+00:00"
}, },
{ {
"name": "intervention/image", "name": "intervention/image",
"version": "3.11.8", "version": "4.2.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Intervention/image.git", "url": "https://github.com/Intervention/image.git",
"reference": "cf04c8dd245697f701057c13d4bfe140d584e738" "reference": "830907fc5397dfc2a51a4e90322d586989fc8364"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Intervention/image/zipball/cf04c8dd245697f701057c13d4bfe140d584e738", "url": "https://api.github.com/repos/Intervention/image/zipball/830907fc5397dfc2a51a4e90322d586989fc8364",
"reference": "cf04c8dd245697f701057c13d4bfe140d584e738", "reference": "830907fc5397dfc2a51a4e90322d586989fc8364",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-mbstring": "*", "ext-mbstring": "*",
"intervention/gif": "^4.2", "intervention/gif": "^5",
"php": "^8.1" "php": "^8.3"
}, },
"require-dev": { "require-dev": {
"mockery/mockery": "^1.6", "mockery/mockery": "^1.6",
"phpstan/phpstan": "^2.1", "phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", "phpunit/phpunit": "^12.0",
"slevomat/coding-standard": "~8.0", "slevomat/coding-standard": "~8.0",
"squizlabs/php_codesniffer": "^4" "squizlabs/php_codesniffer": "^4"
}, },
@ -1680,7 +1680,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/Intervention/image/issues", "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": [ "funding": [
{ {
@ -1696,7 +1696,7 @@
"type": "ko_fi" "type": "ko_fi"
} }
], ],
"time": "2026-05-01T08:20:10+00:00" "time": "2026-07-09T13:07:14+00:00"
}, },
{ {
"name": "jonnybarnes/indieweb", "name": "jonnybarnes/indieweb",

View file

@ -1,5 +1,9 @@
<?php <?php
use Spatie\FlareClient\Sampling\RateSampler;
use Spatie\LaravelFlare\FlareConfig;
use Spatie\LaravelFlare\Senders\LaravelHttpSender;
return [ return [
/* /*
| |
@ -25,7 +29,7 @@ return [
| |
*/ */
'collects' => \Spatie\LaravelFlare\FlareConfig::defaultCollects( 'collects' => FlareConfig::defaultCollects(
ignore: [], ignore: [],
extra: [] extra: []
), ),
@ -74,7 +78,7 @@ return [
*/ */
'sender' => [ 'sender' => [
'class' => \Spatie\LaravelFlare\Senders\LaravelHttpSender::class, 'class' => LaravelHttpSender::class,
'config' => [ 'config' => [
'timeout' => 10, 'timeout' => 10,
], ],
@ -165,7 +169,7 @@ return [
*/ */
'sampler' => [ 'sampler' => [
'class' => \Spatie\FlareClient\Sampling\RateSampler::class, 'class' => RateSampler::class,
'config' => [ 'config' => [
'rate' => env('FLARE_SAMPLER_RATE', 0.1), 'rate' => env('FLARE_SAMPLER_RATE', 0.1),
], ],

View file

@ -1,22 +0,0 @@
<?php
use Intervention\Image\Drivers\Gd\Driver;
return [
/*
|--------------------------------------------------------------------------
| Image Driver
|--------------------------------------------------------------------------
|
| Intervention Image supports "GD Library" and "Imagick" to process images
| internally. You may choose one of them according to your PHP
| configuration. By default PHP's "GD Library" implementation is used.
|
| Supported: "gd", "imagick"
|
*/
'driver' => Driver::class,
];

View file

@ -6,7 +6,6 @@ namespace Tests\Unit\Jobs;
use App\Jobs\ProcessMedia; use App\Jobs\ProcessMedia;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Intervention\Image\ImageManager;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase; use Tests\TestCase;
@ -15,10 +14,9 @@ class ProcessMediaJobTest extends TestCase
#[Test] #[Test]
public function non_media_files_are_not_saved(): void 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'); Storage::disk('local')->put('media/file.txt', 'This is not an image');
$job = new ProcessMedia('file.txt'); $job = new ProcessMedia('media/file.txt');
$job->handle($manager); $job->handle();
$this->assertFileDoesNotExist(storage_path('app/media/').'file.txt'); $this->assertFileDoesNotExist(storage_path('app/media/').'file.txt');
} }
@ -26,10 +24,9 @@ class ProcessMediaJobTest extends TestCase
#[Test] #[Test]
public function small_images_are_not_resized(): void 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')); Storage::disk('local')->put('media/aaron.png', file_get_contents(__DIR__.'/../../aaron.png'));
$job = new ProcessMedia('aaron.png'); $job = new ProcessMedia('media/aaron.png');
$job->handle($manager); $job->handle();
$this->assertFileDoesNotExist(storage_path('app/media/').'aaron.png'); $this->assertFileDoesNotExist(storage_path('app/media/').'aaron.png');
@ -41,10 +38,9 @@ class ProcessMediaJobTest extends TestCase
#[Test] #[Test]
public function large_images_have_smaller_images_created(): void 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')); Storage::disk('local')->put('media/test-image.jpg', file_get_contents(__DIR__.'/../../test-image.jpg'));
$job = new ProcessMedia('media/test-image.jpg'); $job = new ProcessMedia('media/test-image.jpg');
$job->handle($manager); $job->handle();
// These need to look in public disk // These need to look in public disk
Storage::disk('public')->assertExists('media/test-image.jpg'); Storage::disk('public')->assertExists('media/test-image.jpg');