From 1ce30b76e3c70707b0f7cfee1d8d0ba2c48bcd6b Mon Sep 17 00:00:00 2001 From: Thorsten Bus Date: Sun, 31 May 2026 00:08:50 +0200 Subject: [PATCH] feat(service): lazy image resolver --- .sisyphus/evidence/task-5-fallthrough.txt | 16 ++++ .../evidence/task-5-resolution-order.txt | 15 ++++ .../keyvisual-background-nametag/learnings.md | 8 ++ app/Services/ServiceImageResolver.php | 35 +++++++++ tests/Feature/ServiceImageResolverTest.php | 73 +++++++++++++++++++ 5 files changed, 147 insertions(+) create mode 100644 .sisyphus/evidence/task-5-fallthrough.txt create mode 100644 .sisyphus/evidence/task-5-resolution-order.txt create mode 100644 app/Services/ServiceImageResolver.php create mode 100644 tests/Feature/ServiceImageResolverTest.php diff --git a/.sisyphus/evidence/task-5-fallthrough.txt b/.sisyphus/evidence/task-5-fallthrough.txt new file mode 100644 index 0000000..4b64b7b --- /dev/null +++ b/.sisyphus/evidence/task-5-fallthrough.txt @@ -0,0 +1,16 @@ +Task T5 fallthrough evidence + +Covered fallback cases in tests/Feature/ServiceImageResolverTest.php: + +- Service key visual exists in public storage -> returns service filename. +- Service key visual is null and global key visual exists in public storage -> returns global filename. +- Service key visual and global key visual are null -> returns null. +- Service key visual references a missing file, while global key visual exists -> skips missing service file and returns global filename. +- Background resolver covers the same three branches: service file, global fallback, null. + +The implementation uses Storage::disk('public')->exists(...) before returning any referenced filename, so nonexistent paths fall through instead of being returned. + +Verification: +- Initial RED: ddev exec php artisan test tests/Feature/ServiceImageResolverTest.php failed because App\Services\ServiceImageResolver did not exist. +- GREEN: ddev exec php artisan test tests/Feature/ServiceImageResolverTest.php passed after implementation. +- Full suite: ddev exec php artisan test passed with 519 tests / 2627 assertions. diff --git a/.sisyphus/evidence/task-5-resolution-order.txt b/.sisyphus/evidence/task-5-resolution-order.txt new file mode 100644 index 0000000..1888e2c --- /dev/null +++ b/.sisyphus/evidence/task-5-resolution-order.txt @@ -0,0 +1,15 @@ +Task T5 resolution order evidence + +ServiceImageResolver implements lazy raw-filename resolution for service imagery: + +- keyVisualFor(Service $service): checks $service->key_visual_filename first. +- backgroundFor(Service $service): checks $service->background_filename first. +- If the per-service filename is null or missing on Storage::disk('public'), the resolver checks the matching global Setting key. +- Global keys: current_key_visual, current_background. +- Return contract: raw relative filename such as slides/kv.jpg, never /storage/... URL. +- Pure behavior: no model updates, no Setting writes, no Storage writes. + +Verification: +- ddev exec php artisan test tests/Feature/ServiceImageResolverTest.php: PASS (5 tests) +- ddev exec php artisan test: PASS (519 tests, 2627 assertions) +- ddev exec ./vendor/bin/pint app/Services/ServiceImageResolver.php tests/Feature/ServiceImageResolverTest.php: PASS diff --git a/.sisyphus/notepads/keyvisual-background-nametag/learnings.md b/.sisyphus/notepads/keyvisual-background-nametag/learnings.md index 0ec0d84..32aa98c 100644 --- a/.sisyphus/notepads/keyvisual-background-nametag/learnings.md +++ b/.sisyphus/notepads/keyvisual-background-nametag/learnings.md @@ -51,3 +51,11 @@ ## [2026-05-30] Task: T4 - NOTE: namenseinblenderMacro uses snake_case keys (name/uuid/collection_name/collection_uuid) — differs from existing `macroSettings` which uses camelCase (collectionName/collectionUuid). T10/T14 consumers must use snake_case for namenseinblender. - Test file `tests/Feature/NamenseinblenderSettingTest.php` (Pest, 4 tests). Inertia prop assertions use `->where('namenseinblenderMacro.name', ...)` dot-path on shared props from any authed page (settings.index). - 514 app tests green (510 baseline + 4 new). Pint clean. + +## [2026-05-31] Task: T5 +- `App\Services\ServiceImageResolver` is pure and lazy: it only reads `Service`, `Setting::get(...)`, and `Storage::disk('public')->exists(...)`; it writes nothing. +- Resolution order is per-service filename first, then global setting filename, then `null`; missing files are skipped/fall through at both levels. +- `keyVisualFor()` uses `key_visual_filename` then `current_key_visual`; `backgroundFor()` uses `background_filename` then `current_background`. +- Return contract is the raw storage-relative filename (`slides/abc.jpg`), NOT the Service model `/storage/...` URL accessor output. +- Test file `tests/Feature/ServiceImageResolverTest.php` covers service wins, global fallback, null, missing service-file fallthrough, and background resolution branches. +- Full suite green: `ddev exec php artisan test` → 519 passed, 2627 assertions. Pint clean for resolver + tests. diff --git a/app/Services/ServiceImageResolver.php b/app/Services/ServiceImageResolver.php new file mode 100644 index 0000000..eb23306 --- /dev/null +++ b/app/Services/ServiceImageResolver.php @@ -0,0 +1,35 @@ +resolve($service->key_visual_filename, 'current_key_visual'); + } + + public function backgroundFor(Service $service): ?string + { + return $this->resolve($service->background_filename, 'current_background'); + } + + private function resolve(?string $serviceFilename, string $settingKey): ?string + { + if ($serviceFilename !== null && Storage::disk('public')->exists($serviceFilename)) { + return $serviceFilename; + } + + $globalFilename = Setting::get($settingKey); + + if ($globalFilename !== null && Storage::disk('public')->exists($globalFilename)) { + return $globalFilename; + } + + return null; + } +} diff --git a/tests/Feature/ServiceImageResolverTest.php b/tests/Feature/ServiceImageResolverTest.php new file mode 100644 index 0000000..75fe57c --- /dev/null +++ b/tests/Feature/ServiceImageResolverTest.php @@ -0,0 +1,73 @@ +put('slides/kv.jpg', 'fake-content'); + Setting::set('current_key_visual', 'slides/global-kv.jpg'); + + $service = Service::factory()->create([ + 'key_visual_filename' => 'slides/kv.jpg', + ]); + + expect(app(ServiceImageResolver::class)->keyVisualFor($service))->toBe('slides/kv.jpg'); +}); + +test('key visual falls back to global setting when service value is empty', function () { + Storage::disk('public')->put('slides/global-kv.jpg', 'fake-content'); + Setting::set('current_key_visual', 'slides/global-kv.jpg'); + + $service = Service::factory()->create([ + 'key_visual_filename' => null, + ]); + + expect(app(ServiceImageResolver::class)->keyVisualFor($service))->toBe('slides/global-kv.jpg'); +}); + +test('key visual returns null when service and global values are empty', function () { + $service = Service::factory()->create([ + 'key_visual_filename' => null, + ]); + + expect(app(ServiceImageResolver::class)->keyVisualFor($service))->toBeNull(); +}); + +test('key visual skips missing service file and falls through to existing global file', function () { + Storage::disk('public')->put('slides/global-kv.jpg', 'fake-content'); + Setting::set('current_key_visual', 'slides/global-kv.jpg'); + + $service = Service::factory()->create([ + 'key_visual_filename' => 'slides/kv-missing.jpg', + ]); + + expect(app(ServiceImageResolver::class)->keyVisualFor($service))->toBe('slides/global-kv.jpg'); +}); + +test('background uses service file falls back to global and returns null when none exist', function () { + Storage::disk('public')->put('slides/bg.jpg', 'fake-content'); + Setting::set('current_background', 'slides/global-bg.jpg'); + + $service = Service::factory()->create([ + 'background_filename' => 'slides/bg.jpg', + ]); + + expect(app(ServiceImageResolver::class)->backgroundFor($service))->toBe('slides/bg.jpg'); + + Storage::disk('public')->delete('slides/bg.jpg'); + Storage::disk('public')->put('slides/global-bg.jpg', 'fake-content'); + $service->update(['background_filename' => null]); + + expect(app(ServiceImageResolver::class)->backgroundFor($service->refresh()))->toBe('slides/global-bg.jpg'); + + Storage::disk('public')->delete('slides/global-bg.jpg'); + Setting::set('current_background', null); + + expect(app(ServiceImageResolver::class)->backgroundFor($service->refresh()))->toBeNull(); +});