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.
|
||||
- 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.
|
||||
|
|
|
|||
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