feat(service): lazy image resolver
This commit is contained in:
parent
38e79553eb
commit
1ce30b76e3
16
.sisyphus/evidence/task-5-fallthrough.txt
Normal file
16
.sisyphus/evidence/task-5-fallthrough.txt
Normal file
|
|
@ -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.
|
||||||
15
.sisyphus/evidence/task-5-resolution-order.txt
Normal file
15
.sisyphus/evidence/task-5-resolution-order.txt
Normal file
|
|
@ -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
|
||||||
|
|
@ -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.
|
- 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).
|
- 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.
|
- 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.
|
||||||
|
|
|
||||||
35
app/Services/ServiceImageResolver.php
Normal file
35
app/Services/ServiceImageResolver.php
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Service;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class ServiceImageResolver
|
||||||
|
{
|
||||||
|
public function keyVisualFor(Service $service): ?string
|
||||||
|
{
|
||||||
|
return $this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
73
tests/Feature/ServiceImageResolverTest.php
Normal file
73
tests/Feature/ServiceImageResolverTest.php
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Service;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use App\Services\ServiceImageResolver;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
Storage::fake('public');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('key visual uses service file when present', function () {
|
||||||
|
Storage::disk('public')->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();
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue