From 88972ba31261c628943d88f590a3c3cad4c29158 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 08:51:04 +0000 Subject: [PATCH] feat(core): take aspects from the application's PSR-11 container AspectContainer::setDelegate() attaches the PSR-11 container of the application once it exists: get()/has() fall back to it for ids that are not registered here, guarded against composite containers that ask back. addDelegatedService() registers a lazy service whose instance is fetched from the delegate on first use, so aspects with dependencies are declared in configureAop() while weaving and cache warm-up never need the delegate. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JzyTe7njn5wEgo3sGxfjYR --- CHANGELOG.md | 1 + README.md | 3 + UPGRADE-4.0.md | 4 +- docs/configuration.md | 33 ++++ src/Core/AGENTS.md | 3 +- src/Core/AspectContainer.php | 32 +++- src/Core/Container.php | 68 ++++++++- tests/Core/ContainerTest.php | 142 ++++++++++++++++++ .../Fixtures/project/bin/delegated-aspect.php | 27 ++++ .../project/src/Application/Greeter.php | 15 ++ .../project/src/Aspect/GreetingAspect.php | 24 +++ .../src/Container/ApplicationContainer.php | 37 +++++ .../ApplicationServiceNotFoundException.php | 9 ++ .../src/Kernel/DefaultAspectKernel.php | 3 + tests/Functional/DelegatedServiceTest.php | 47 ++++++ .../ProductionDelegatedServiceTest.php | 24 +++ 16 files changed, 468 insertions(+), 4 deletions(-) create mode 100644 tests/Fixtures/project/bin/delegated-aspect.php create mode 100644 tests/Fixtures/project/src/Application/Greeter.php create mode 100644 tests/Fixtures/project/src/Aspect/GreetingAspect.php create mode 100644 tests/Fixtures/project/src/Container/ApplicationContainer.php create mode 100644 tests/Fixtures/project/src/Container/ApplicationServiceNotFoundException.php create mode 100644 tests/Functional/DelegatedServiceTest.php create mode 100644 tests/Functional/ProductionDelegatedServiceTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 4be427d7..e0897552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ See [UPGRADE-4.0.md](UPGRADE-4.0.md) for the migration guide. * [Feature] **Attributes are propagated to proxies** — attributes of the woven class, its methods, parameters, constants and properties are copied from the source AST (arguments included, not evaluated values), so runtime attribute inspection of a proxy returns the original attributes (e.g. `#[\Deprecated]`). * [Feature] [BC BREAK] **Interface-keyed registration listeners** — new `AspectContainer::onRegistration(string $interfaceFQCN, Closure $listener)`: the listener receives the id (class-name) and the container whenever a deferred service whose id implements the interface is registered via `addLazyService()`; matching never autoloads, so an id whose class is not loaded yet reaches the listener as a candidate. Values are never touched, so laziness is preserved; with no listeners armed (production) registration stays a pure array write. The kernel arms one `Aspect::class` listener in debug mode to track aspect source files as freshness resources at registration time, resolving them through composer without loading the aspects (previously hardwired into `registerAspect()`). `AspectContainer::addResource()` is now part of the interface and `final public` on `Container` (was `final protected`). * [Feature] [BC BREAK] **PSR-11 container** — `AspectContainer` extends `Psr\Container\ContainerInterface`: `getValue()` is renamed to `get()`, `has()` is inherited from the PSR interface. `ServiceNotFoundException` implements `NotFoundExceptionInterface`, and a container entry of the wrong type in `getService()` throws the new `Go\Aop\Exception\ContainerException` (an `InvalidConfigurationException` implementing `ContainerExceptionInterface`). +* [Feature] **Aspects from the application container** — `AspectContainer::setDelegate(ContainerInterface $delegate)` attaches the PSR-11 container of the application: `get()`/`has()` fall back to it for unknown ids, and `addDelegatedService(class-string $id, ?string $delegateId = null)` registers a service (typically an aspect with dependencies) whose instance is fetched from the delegate on first use. Registration happens in `configureAop()`, the delegate is attached later, once the application has built its container; weaving and cache warm-up never need it. * [Feature] Console commands declare their name, description and help with `#[AsCommand]`, `bin/aspect` loads them lazily, and every command returns `Command::SUCCESS`/`Command::FAILURE` (`debug:weaving` used to return the raw error count). `cache:warmup:aop` stops cleanly on SIGINT/SIGTERM. * [Feature] [BC BREAK] **Doctrine bridge rewritten for the trait-based engine (#671)** — new `Go\Bridge\Doctrine\WovenEntityClassLocator` lets Doctrine mapping drivers (ORM 3.6+) discover entities through the autoloader, so the woven class is mapped instead of the original source, and entities loaded before discovery are no longer missed. `MetadataLoadInterceptor` no longer rewrites metadata (the old mapped-superclass workaround only matched the extend-based engine); it now reports woven entities with intercepted mapped properties when Doctrine native lazy objects are disabled. It is registered as a plain listener: `$eventManager->addEventListener(Events::loadClassMetadata, new MetadataLoadInterceptor())` (it is no longer an `EventSubscriber`). diff --git a/README.md b/README.md index aa4334b9..bcdd6f1d 100644 --- a/README.md +++ b/README.md @@ -333,6 +333,9 @@ use Go\Core\AspectContainer; $container->addLazyService(MonitorAspect::class, static fn() => new MonitorAspect()); // Eager alternative: $container->add(MonitorAspect::class, new MonitorAspect()); + + // Built by the PSR-11 container of your application, attached later via setDelegate(): + // $container->addDelegatedService(MonitorAspect::class); } //... diff --git a/UPGRADE-4.0.md b/UPGRADE-4.0.md index be056f29..676d8a0d 100644 --- a/UPGRADE-4.0.md +++ b/UPGRADE-4.0.md @@ -118,7 +118,9 @@ deferred registrations of services implementing an interface. `AspectContainer` extends `Psr\Container\ContainerInterface`, so the container can be passed to any code that expects a PSR-11 container. `getValue()` is renamed to `get()`, and `has()` comes from the PSR -interface with the same signature. A custom `containerClass` implements `get()` instead of `getValue()`. +interface with the same signature. A custom `containerClass` implements `get()` instead of `getValue()`, plus +`setDelegate()` and `addDelegatedService()`, which take aspects from the container of your application +(see [Aspects from the application container](docs/configuration.md#aspects-from-the-application-container)). ```php // before diff --git a/docs/configuration.md b/docs/configuration.md index a21b531d..bbccaa88 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -42,6 +42,39 @@ the matching namespaces. An `initialization(...)` pointcut turns on the rewritin woven files; while one is registered, every rewritten `new` goes through the interceptor, so keep such pointcuts to the code that needs them. +## Aspects from the application container + +The aspect container is a PSR-11 container, and it can delegate to the container of your application +(Symfony, Laravel, PHP-DI or any other PSR-11 container), so aspects get their dependencies from there. +The kernel is initialized before the application builds its container, so the two steps are split: + +```php +// configureAop(): the aspect is known by its class name, its instance comes from the application container +protected function configureAop(AspectContainer $container): void +{ + $container->addDelegatedService(MonitorAspect::class); + // or, when the application knows the service by another id: + // $container->addDelegatedService(MonitorAspect::class, 'app.monitor_aspect'); +} + +// Once the application container exists, e.g. right after the framework kernel has booted +ApplicationAspectKernel::getInstance()->getContainer()->setDelegate($psrContainer); +``` + +Weaving and the cache warm-up only need the class of the aspect, so they run before `setDelegate()`, even in +`bin/aspect`. The instance is fetched from the application container on the first advice call, and must be +an instance of the registered class. Using it before `setDelegate()` throws `ContainerException`. + +- `get()` and `has()` fall back to the delegate for ids that are not registered in the aspect container, so the + factories passed to `addLazyService()` can resolve their dependencies through it as well. +- Aspects are never discovered in the delegate: PSR-11 cannot list services, so every aspect is registered in + `configureAop()`. +- An aspect without instance properties cannot be created lazily, so it is built when the aspects are + collected for weaving, before the delegate exists. Such an aspect has no dependencies to inject, so register + it with `add()` or `addLazyService()`. +- The aspect container hands out a native lazy proxy that forwards to the instance of the application container: + the state is shared, the identity (`===`) is not. + ## Extending the transformation pipeline Every file is transformed by a chain of `Go\Instrument\Transformer\SourceTransformer` services, in registration diff --git a/src/Core/AGENTS.md b/src/Core/AGENTS.md index 7440801f..18754a7c 100644 --- a/src/Core/AGENTS.md +++ b/src/Core/AGENTS.md @@ -1,7 +1,8 @@ # src/Core — Container and aspect loading ## Container (Container.php) -- Generic, AOP-unaware DI container, PSR-11 (AspectContainer extends Psr\Container\ContainerInterface): add(by class-string|key), get(), has(), getService(), addLazyService(Closure), onRegistration(interfaceFQCN, listener), addResource() +- Generic, AOP-unaware DI container, PSR-11 (AspectContainer extends Psr\Container\ContainerInterface): add(by class-string|key), get(), has(), getService(), addLazyService(Closure), addDelegatedService(), setDelegate(), onRegistration(interfaceFQCN, listener), addResource() +- Delegate (app's PSR-11 container, attached after init): get()/has() fall back to it on a miss only (hot path unchanged, delegated values are not stored); addDelegatedService() = addLazyService() whose factory fetches from the delegate, so registration/weaving/warm-up never need it. Property-less classes can't be lazy → built at materialization → ContainerException without delegate. getServicesByInterface() never enumerates the delegate - Automatic tagging by interface; deferred services materialize as native lazy proxies via NativeLazyProxy (engine probe, no userland compatibility predicate) - add() runs on every request: tags via class_implements(), the class file of an added object becomes a resource only on the next isFreshSince() (no reflection or stat calls at registration) - onRegistration() never autoloads: ids of loaded classes are matched exactly, ids not loaded yet reach the listener as candidates diff --git a/src/Core/AspectContainer.php b/src/Core/AspectContainer.php index 946729af..8a6044ae 100644 --- a/src/Core/AspectContainer.php +++ b/src/Core/AspectContainer.php @@ -14,6 +14,7 @@ use Closure; use Go\Aop\Exception\ContainerException; +use Go\Aop\Exception\InvalidConfigurationException; use Go\Aop\Exception\ServiceNotFoundException; use Psr\Container\ContainerInterface; @@ -104,7 +105,7 @@ public function getServicesByInterface(string $interfaceTagClassName): array; * @param string $id Identifier of the entry * * @return ($id is class-string ? T : mixed) - * @throws ServiceNotFoundException if no entry was found for the identifier + * @throws ServiceNotFoundException if no entry was found for the identifier, neither here nor in the delegate * * @template T of object */ @@ -161,6 +162,35 @@ public function add(string $id, mixed $value): void; */ public function addLazyService(string $id, Closure $lazyInitializationClosure): void; + /** + * Adds a deferred service that is built by the delegate container ({@see setDelegate()}). + * + * The service is registered like {@see addLazyService()}: it is known by its id (an aspect + * is collected through its interface tag), but its instance is fetched from the delegate + * container on first use only. So the service can be registered in configureAop(), long + * before the application builds its own container and attaches it via setDelegate(). + * A class without instance properties can not be created lazily: its instance is fetched + * as soon as the service materializes, e.g. when aspects are collected for weaving. + * + * @param class-string $id Identifier of the service, must be equal to the class-name + * @param string|null $delegateId Identifier of the service in the delegate container, defaults to $id + * + * @template T of object + */ + public function addDelegatedService(string $id, ?string $delegateId = null): void; + + /** + * Attaches a delegate container, e.g. the PSR-11 container of the application. + * + * {@see get()} and {@see has()} fall back to the delegate for ids that are not registered in + * this container, and services added via {@see addDelegatedService()} are fetched from it. + * Services are never enumerated from the delegate: {@see getServicesByInterface()} only + * returns services registered in this container. + * + * @throws InvalidConfigurationException if the container is given as its own delegate + */ + public function setDelegate(ContainerInterface $delegate): void; + /** * Adds a link to the file resource into the container * diff --git a/src/Core/Container.php b/src/Core/Container.php index c8fc3d71..d722fa08 100644 --- a/src/Core/Container.php +++ b/src/Core/Container.php @@ -16,6 +16,7 @@ use Go\Aop\Exception\ContainerException; use Go\Aop\Exception\InvalidConfigurationException; use Go\Aop\Exception\ServiceNotFoundException; +use Psr\Container\ContainerInterface; use ReflectionClass; /** @@ -28,6 +29,17 @@ final class Container implements AspectContainer */ private array $values = []; + /** + * Container that ids unknown here are looked up in, and that builds the delegated services + */ + private ?ContainerInterface $delegate = null; + + /** + * Set while the delegate is asked for an id: a delegate composed of this container (a composite + * container) asks this container back, which must not delegate again + */ + private bool $isDelegating = false; + /** * @var array Deferred service factories, keyed by class-name */ @@ -147,6 +159,14 @@ public function get(string $id): mixed if (!isset($this->values[$id]) && !array_key_exists($id, $this->values)) { if (isset($this->factories[$id])) { $this->materializeService($id); + } elseif ($this->delegate !== null && $this->delegateHas($id)) { + // Delegated values are not stored: the delegate owns their lifecycle + $this->isDelegating = true; + try { + return $this->delegate->get($id); + } finally { + $this->isDelegating = false; + } } else { throw new ServiceNotFoundException("Value $id is not defined in the container"); } @@ -157,7 +177,53 @@ public function get(string $id): mixed public function has(string $id): bool { - return isset($this->values[$id]) || array_key_exists($id, $this->values) || isset($this->factories[$id]); + return isset($this->values[$id]) + || array_key_exists($id, $this->values) + || isset($this->factories[$id]) + || $this->delegateHas($id); + } + + private function delegateHas(string $id): bool + { + if ($this->delegate === null || $this->isDelegating) { + return false; + } + $this->isDelegating = true; + try { + return $this->delegate->has($id); + } finally { + $this->isDelegating = false; + } + } + + public function addDelegatedService(string $id, ?string $delegateId = null): void + { + $delegateId ??= $id; + $this->addLazyService($id, function () use ($id, $delegateId): object { + if ($this->delegate === null) { + throw new ContainerException( + "Service $id is built by the delegate container, but no delegate is set yet: call setDelegate() " + . 'before the service is used. A class without instance properties can not be created lazily ' + . 'and is built as soon as aspects are collected, register it with add() or addLazyService() instead', + ); + } + $service = $this->delegate->get($delegateId); + if (!$service instanceof $id) { + throw new ContainerException( + 'Delegate container returned ' . get_debug_type($service) . " for service $id ($delegateId)", + ); + } + + return $service; + }); + } + + public function setDelegate(ContainerInterface $delegate): void + { + if ($delegate === $this) { + throw new InvalidConfigurationException('Container can not be its own delegate'); + } + $this->delegate = $delegate; } public function getServicesByInterface(string $interfaceTagClassName): array diff --git a/tests/Core/ContainerTest.php b/tests/Core/ContainerTest.php index ca0a2b5c..36b72821 100644 --- a/tests/Core/ContainerTest.php +++ b/tests/Core/ContainerTest.php @@ -14,7 +14,9 @@ use Go\Aop\Advisor; use Go\Aop\Aspect; +use Go\Aop\Exception\ContainerException; use Go\Aop\Exception\InvalidConfigurationException; +use Go\Aop\Exception\ServiceNotFoundException; use Go\Aop\Pointcut; use Go\Aop\Pointcut\PointcutLexer; use Go\Aop\Pointcut\PointcutParser; @@ -24,6 +26,7 @@ use Go\Stubs\ReentrantAutoloadAspect; use Go\Tests\TestProject\Aspect\DoSomethingAspect; use Go\Tests\TestProject\Aspect\EnumMethodAspect; +use Go\Tests\TestProject\Container\ApplicationContainer; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; @@ -237,6 +240,145 @@ public function testGetServiceThrowsPsrContainerExceptionOnTypeMismatch(): void $this->container->getService(First::class); } + public function testGetAndHasFallBackToDelegate(): void + { + $this->container->setDelegate(new ApplicationContainer(['app.service' => static fn(): stdClass => new stdClass()])); + + $this->assertTrue($this->container->has('app.service')); + $this->assertSame($this->container->get('app.service'), $this->container->get('app.service')); + $this->assertFalse($this->container->has('missing')); + + $this->expectException(ServiceNotFoundException::class); + $this->container->get('missing'); + } + + public function testLocalEntryTakesPrecedenceOverDelegate(): void + { + $local = new stdClass(); + $this->container->add('shared', $local); + $this->container->setDelegate(new ApplicationContainer(['shared' => static fn(): stdClass => new stdClass()])); + + $this->assertSame($local, $this->container->get('shared')); + } + + public function testContainerCanNotBeItsOwnDelegate(): void + { + $this->expectException(InvalidConfigurationException::class); + $this->container->setDelegate($this->container); + } + + public function testCompositeDelegateContainingThisContainerDoesNotLoop(): void + { + $application = new ApplicationContainer(['app.service' => static fn(): stdClass => new stdClass()]); + // PSR-11 delegate lookup: a composite of the aspect container and the application container + $composite = new class ([$this->container, $application]) implements ContainerInterface { + /** + * @param list $containers + */ + public function __construct(private readonly array $containers) {} + + public function get(string $id): mixed + { + foreach ($this->containers as $container) { + if ($container->has($id)) { + return $container->get($id); + } + } + throw new ServiceNotFoundException("Value $id is not defined in the composite container"); + } + + public function has(string $id): bool + { + return array_any($this->containers, static fn(ContainerInterface $container): bool => $container->has($id)); + } + }; + $this->container->setDelegate($composite); + + $this->assertTrue($this->container->has('app.service')); + $this->assertSame($application->get('app.service'), $this->container->get('app.service')); + $this->assertFalse($composite->has('missing')); + $this->assertFalse($this->container->has('missing')); + + $this->expectException(ServiceNotFoundException::class); + $this->container->get('missing'); + } + + public function testDelegatedServiceIsFetchedFromDelegateAttachedLater(): void + { + $this->container->addDelegatedService(StatefulTestAspect::class); + // Collecting aspects (as the weaver does) needs no delegate yet: the service is a lazy proxy + $aspects = $this->container->getServicesByInterface(Aspect::class); + $this->assertArrayHasKey(StatefulTestAspect::class, $aspects); + + $built = new \ArrayObject(); + $application = new ApplicationContainer([ + StatefulTestAspect::class => static function () use ($built): StatefulTestAspect { + $built->append(StatefulTestAspect::class); + + return new StatefulTestAspect(42); + }, + ]); + $this->container->setDelegate($application); + $this->assertCount(0, $built, 'Delegated service must not be built before its first use'); + + $this->assertSame(42, $this->container->getService(StatefulTestAspect::class)->getState()); + $this->assertCount(1, $built); + $this->assertSame($application->get(StatefulTestAspect::class), $this->getRealInstance(StatefulTestAspect::class)); + } + + public function testDelegatedServiceUsesGivenDelegateId(): void + { + $this->container->addDelegatedService(StatefulTestAspect::class, 'app.stateful_aspect'); + $this->container->setDelegate(new ApplicationContainer([ + 'app.stateful_aspect' => static fn(): StatefulTestAspect => new StatefulTestAspect(7), + ])); + + $this->assertSame(7, $this->container->getService(StatefulTestAspect::class)->getState()); + } + + public function testDelegatedServiceUsedBeforeDelegateIsSetFails(): void + { + $this->container->addDelegatedService(StatefulTestAspect::class); + $aspect = $this->container->getService(StatefulTestAspect::class); + + $this->expectException(ContainerException::class); + $this->expectExceptionMessageMatches('/no delegate is set yet/'); + $aspect->getState(); + } + + public function testDelegatedServiceOfWrongTypeFailsOnFirstUse(): void + { + $this->container->addDelegatedService(StatefulTestAspect::class); + $this->container->setDelegate(new ApplicationContainer([StatefulTestAspect::class => static fn(): stdClass => new stdClass()])); + $aspect = $this->container->getService(StatefulTestAspect::class); + + $this->expectException(ContainerException::class); + $this->expectExceptionMessageMatches('/Delegate container returned stdClass/'); + $aspect->getState(); + } + + public function testPropertylessDelegatedServiceNeedsDelegateAtMaterialization(): void + { + // A class without instance properties can not be lazy: it is built as soon as it materializes + $this->container->addDelegatedService(DoSomethingAspect::class); + + $this->expectException(ContainerException::class); + $this->expectExceptionMessageMatches('/no delegate is set yet/'); + $this->container->getService(DoSomethingAspect::class); + } + + /** + * Returns the instance a lazy proxy of the container forwards to + * + * @param class-string $id + */ + private function getRealInstance(string $id): object + { + $service = $this->container->getService($id); + + return new \ReflectionClass($id)->initializeLazyObject($service); + } + public function testGetServiceEnsuresThatKeyAndReturnedTypeMatches(): void { $this->expectException(InvalidConfigurationException::class); diff --git a/tests/Fixtures/project/bin/delegated-aspect.php b/tests/Fixtures/project/bin/delegated-aspect.php new file mode 100644 index 00000000..f63c11ea --- /dev/null +++ b/tests/Fixtures/project/bin/delegated-aspect.php @@ -0,0 +1,27 @@ + static fn(): GreetingAspect => new GreetingAspect('Hello,'), +]); +$aspectContainer = AspectKernel::getInstance()->getContainer(); +$aspectContainer->setDelegate($applicationContainer); + +echo json_encode([ + 'greeting' => (new Greeter())->greet('World'), + // The aspect container hands out a lazy proxy that forwards to the application's instance + 'delegated' => $aspectContainer->has(GreetingAspect::class), +], JSON_THROW_ON_ERROR); diff --git a/tests/Fixtures/project/src/Application/Greeter.php b/tests/Fixtures/project/src/Application/Greeter.php new file mode 100644 index 00000000..c0235b10 --- /dev/null +++ b/tests/Fixtures/project/src/Application/Greeter.php @@ -0,0 +1,15 @@ +greet(*))')] + public function aroundGreet(MethodInvocation $invocation): mixed + { + $result = $invocation->proceed(); + + return is_string($result) ? $this->salutation . ' ' . $result : $result; + } +} diff --git a/tests/Fixtures/project/src/Container/ApplicationContainer.php b/tests/Fixtures/project/src/Container/ApplicationContainer.php new file mode 100644 index 00000000..dac8ff5d --- /dev/null +++ b/tests/Fixtures/project/src/Container/ApplicationContainer.php @@ -0,0 +1,37 @@ + Services built so far, shared between lookups + */ + private array $services = []; + + /** + * @param array $factories + */ + public function __construct(private readonly array $factories) {} + + public function get(string $id): object + { + if (!isset($this->factories[$id])) { + throw new ApplicationServiceNotFoundException("Service $id is not defined in the application container"); + } + + return $this->services[$id] ??= ($this->factories[$id])(); + } + + public function has(string $id): bool + { + return isset($this->factories[$id]); + } +} diff --git a/tests/Fixtures/project/src/Container/ApplicationServiceNotFoundException.php b/tests/Fixtures/project/src/Container/ApplicationServiceNotFoundException.php new file mode 100644 index 00000000..e19cc784 --- /dev/null +++ b/tests/Fixtures/project/src/Container/ApplicationServiceNotFoundException.php @@ -0,0 +1,9 @@ +addLazyService(AlphaCollisionAspect::class, fn(): AlphaCollisionAspect => new AlphaCollisionAspect()); $container->addLazyService(BetaCollisionAspect::class, fn(): BetaCollisionAspect => new BetaCollisionAspect()); $container->addLazyService(EdgeCaseAspect::class, fn(): EdgeCaseAspect => new EdgeCaseAspect()); + // Built by the application container (see bin/delegated-aspect.php), which only exists after the kernel is up + $container->addDelegatedService(GreetingAspect::class); } } diff --git a/tests/Functional/DelegatedServiceTest.php b/tests/Functional/DelegatedServiceTest.php new file mode 100644 index 00000000..8a0e2af4 --- /dev/null +++ b/tests/Functional/DelegatedServiceTest.php @@ -0,0 +1,47 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Functional; + +use Go\Tests\TestProject\Application\Greeter; +use Symfony\Component\Process\PhpExecutableFinder; +use Symfony\Component\Process\Process; + +/** + * An aspect built by the application container is woven without that container (cache warm-up runs + * before it exists) and is fetched from it on first use, once the application attached it as delegate + */ +class DelegatedServiceTest extends BaseFunctionalTestCase +{ + public function testAspectFromApplicationContainerIsApplied(): void + { + $this->assertMethodWoven(Greeter::class, 'greet', 'Go\\Tests\\TestProject\\Aspect\\GreetingAspect->aroundGreet'); + + $phpExecutable = (new PhpExecutableFinder())->find(); + $this->assertIsString($phpExecutable); + $process = new Process( + [$phpExecutable, __DIR__ . '/../Fixtures/project/bin/delegated-aspect.php'], + null, + ['GO_AOP_CONFIGURATION' => $this->getConfigurationName()], + ); + $process->run(); + $this->assertTrue($process->isSuccessful(), $process->getOutput() . $process->getErrorOutput()); + + $result = json_decode($process->getOutput(), true, flags: JSON_THROW_ON_ERROR); + $this->assertSame(['greeting' => 'Hello, World!', 'delegated' => true], $result); + } + + protected function getConfigurationName(): string + { + return 'default'; + } +} diff --git a/tests/Functional/ProductionDelegatedServiceTest.php b/tests/Functional/ProductionDelegatedServiceTest.php new file mode 100644 index 00000000..c39cfc28 --- /dev/null +++ b/tests/Functional/ProductionDelegatedServiceTest.php @@ -0,0 +1,24 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Functional; + +/** + * Same as DelegatedServiceTest with debug mode off: woven code and advisors are served from the warmed-up cache + */ +class ProductionDelegatedServiceTest extends DelegatedServiceTest +{ + protected function getConfigurationName(): string + { + return 'production'; + } +}