From c6fda5f6314d0834a2c97df665c23b29d2319d2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20M=C3=A4ki?= Date: Fri, 12 Jun 2026 00:05:43 +0200 Subject: [PATCH] Resolve decorated class through container in initDecorated() Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 2 +- README.md | 2 ++ src/CacheDecorator.php | 11 +++++---- src/tests/CachedStubServiceTest.php | 13 +++++++++++ .../Stubs/CachedStubServiceWithDependency.php | 23 +++++++++++++++++++ src/tests/Stubs/StubCollaborator.php | 15 ++++++++++++ src/tests/Stubs/StubServiceWithDependency.php | 17 ++++++++++++++ 7 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 src/tests/Stubs/CachedStubServiceWithDependency.php create mode 100644 src/tests/Stubs/StubCollaborator.php create mode 100644 src/tests/Stubs/StubServiceWithDependency.php diff --git a/CLAUDE.md b/CLAUDE.md index 015fad9..c3d9f19 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,7 +29,7 @@ The package is intentionally small — four production files plus tests. - **Config** is read in `getConfig()` from `{$this->config_key}.*` (`ttl`, `enabled`, `use_tags`) plus `app.debug` for the `$debug` flag controlling `Log::debug` output. The base default is `'cache_decorator'`; `RepositoryCacheDecorator` overrides it to `'repository_cache'`. - **`src/ServiceProvider.php`** — publishes both config files: `config/cache_decorator.php` under the `cache-decorator-config` tag and `config/repository_cache.php` under the `repository-cache-config` tag; `register()` is empty. The provider is auto-discovered via `extra.laravel.providers` in `composer.json`. -The classes use Laravel facades (`Cache`, `Config`, `Log` from `Illuminate\Support\Facades`), so this package is Laravel-coupled — the TODO in the header notes a future goal to decouple. When testing the no-arg construction path, the decorated class is instantiated via `new $class` inside `initDecorated()` unless an instance is injected through the constructor. +The classes use Laravel facades (`Cache`, `Config`, `Log` from `Illuminate\Support\Facades`), so this package is Laravel-coupled — the TODO in the header notes a future goal to decouple. When testing the no-arg construction path, the decorated class is resolved through Laravel's service container via `resolve($class)` inside `initDecorated()` unless an instance is injected through the constructor. Because resolution goes through the container, the decorated class may declare auto-wired constructor dependencies and `decoratedClass()` may return a container-bound interface. ## Conventions diff --git a/README.md b/README.md index 7d457b5..1752acf 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,8 @@ class CachedReportingService extends CacheDecorator { $cached = new CachedReportingService; ``` +The FQCN returned by `decoratedClass()` is resolved through Laravel's service container (via `resolve()`), so the decorated class may declare constructor dependencies (they are auto-wired), and `decoratedClass()` may return an interface that's bound in the container. + ### Custom caching logic for a single method If a particular method needs hand-tuned caching, override it in the subclass and use the protected helpers: diff --git a/src/CacheDecorator.php b/src/CacheDecorator.php index 0fa9b02..8f9eb9d 100644 --- a/src/CacheDecorator.php +++ b/src/CacheDecorator.php @@ -93,9 +93,12 @@ protected function cacheMiss(): object } /** - * Override to return the FQCN of the class to default-instantiate when no - * instance is passed to the constructor. Return null (the default) to - * require an instance via the constructor. + * Override to return the FQCN of the class to resolve when no instance is + * passed to the constructor. The FQCN is resolved through Laravel's service + * container via resolve(), so the decorated class may declare auto-wired + * constructor dependencies, and this may return an interface bound in the + * container. Return null (the default) to require an instance via the + * constructor. * * @return class-string|null FQCN of the decorated class, or null */ @@ -182,7 +185,7 @@ public function initDecorated(?object $decorated): void ); } - $decorated = new $class; + $decorated = resolve($class); } $this->decorated = $decorated; diff --git a/src/tests/CachedStubServiceTest.php b/src/tests/CachedStubServiceTest.php index c58c05c..2fdeb83 100644 --- a/src/tests/CachedStubServiceTest.php +++ b/src/tests/CachedStubServiceTest.php @@ -9,6 +9,7 @@ use Trm42\CacheDecorator\ServiceProvider; use Trm42\CacheDecorator\Tests\Stubs\CachedAutoStubService; use Trm42\CacheDecorator\Tests\Stubs\CachedStubService; +use Trm42\CacheDecorator\Tests\Stubs\CachedStubServiceWithDependency; use Trm42\CacheDecorator\Tests\Stubs\StubService; /** @@ -105,6 +106,18 @@ public function test_no_arg_construction_via_decorated_class() $this->assertEquals(['id' => 7, 'name' => 'thing-7'], $result); } + #[Test] + public function test_decorated_class_is_resolved_through_container_with_dependencies() + { + // StubServiceWithDependency requires a StubCollaborator constructor + // argument, so `new $class` would fail. Resolving through the container + // auto-wires the dependency. + $service = new CachedStubServiceWithDependency; + $service->setTtl(300); + + $this->assertEquals('hello from collaborator', $service->delegatedGreeting()); + } + #[Test] public function test_constructor_without_instance_or_decorated_class_throws() { diff --git a/src/tests/Stubs/CachedStubServiceWithDependency.php b/src/tests/Stubs/CachedStubServiceWithDependency.php new file mode 100644 index 0000000..21a6d14 --- /dev/null +++ b/src/tests/Stubs/CachedStubServiceWithDependency.php @@ -0,0 +1,23 @@ + + */ +class CachedStubServiceWithDependency extends CacheDecorator +{ + protected ?string $prefix_key = 'dep-svc'; + + #[\Override] + protected function decoratedClass(): ?string + { + return StubServiceWithDependency::class; + } +} diff --git a/src/tests/Stubs/StubCollaborator.php b/src/tests/Stubs/StubCollaborator.php new file mode 100644 index 0000000..09ed558 --- /dev/null +++ b/src/tests/Stubs/StubCollaborator.php @@ -0,0 +1,15 @@ +collaborator->greeting(); + } +}