diff --git a/CHANGELOG.md b/CHANGELOG.md index f9efcff1..e58e5cd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,8 @@ Changelog * [Removed] `SelfValueTransformer` and `SelfValueVisitor` — no longer needed with the trait-based engine. * [Performance] **Direct static joinpoint initialization** — leveraging PHP 8.3+ support for dynamic expressions in static variable initializers, all generated proxy method bodies now initialize their static joinpoint variables directly. * [Performance] [BC BREAK] **Truly lazy container services** — `AspectContainer::addLazyService()` stores a factory instead of building a PHP 8.4 lazy proxy, so registering the built-in services no longer reflects/autoloads them on every request; a service is constructed on first retrieval. Code relying on lazy-proxy instances being available from boot must use `getService()` instead. -* [Performance] [BC BREAK] **Lazy aspect registration** — `AspectContainer::registerAspect()` accepts an aspect class-name plus an optional factory closure (required for aspects with constructor dependencies); such aspects are constructed on first use instead of during `configureAop()`. The signature widened to `registerAspect(Aspect|string $aspectOrClassName, ?Closure $aspectFactory = null)`; instance registration behaves as before. +* [Performance] [BC BREAK] **Aspects are typical container services; `registerAspect()` removed** — the container is now a fully AOP-unaware DI implementation. Register aspects in `configureAop()` through the generic API: `$container->add(Foo::class, new Foo())` for an eager instance, or `$container->addLazyService(Foo::class, static fn() => new Foo(...))` for deferred construction on first use (first advice hit or first real interaction with the lazy object). `AspectContainer::registerAspect()`, the internal per-aspect validators and the double per-aspect validation pass are gone; a factory returning an incompatible object now fails on first use via the container's `instanceof` guard instead of at retrieval. Aspect enumeration on the weaving path is unchanged (`getServicesByInterface(Aspect::class)` finds deferred aspects by id). +* [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()`. 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 (previously hardwired into `registerAspect()`). `AspectContainer::addResource()` is now part of the interface and `final public` on `Container` (was `final protected`). * [Performance] [BC BREAK] **Class-keyed runtime cache map, integrated into composer** — the weaver records the FQCN of every discovered class into the cache metadata, and the runtime cache file `_include.cache` holds a woven-class => cached-file map plus a skip set of known untransformed classes. At production boot the class map is handed to composer via `ClassLoader::addClassMap()`, so woven classes resolve natively to their cached files (composer consults the class map before PSR-4) with no per-class `realpath()`; untransformed classes are served untouched. `_transformation.cache` keeps the full build metadata and is loaded lazily, only on the cache-miss/weaving paths. **Cache format bump**: a pre-4.0 cache directory carries no class names and is treated as stale — everything re-weaves once (or run `cache:warmup:aop` at deploy). New accessors: `CachePathManager::queryClassMap()`, `querySkippedClasses()`, `registerClassForResource()`. * [Performance] [BC BREAK] **`PREBUILT_CACHE` now really trusts the cache** — with `Features::PREBUILT_CACHE` enabled, an existing cache record is used without any freshness checks: cache directory existence/writability probes, source `filemtime` comparisons, tracked-resource checks and advisor cache freshness are all skipped (previously the flag only skipped one writability check). Build the cache at deploy time (`bin/aspect cache:warmup:aop`); staleness is the deployer's responsibility. An advisor cache file of an incompatible (older) format falls back to the direct loader without writing (read-only file systems stay safe); a corrupt, non-includable file throws. * [Performance] [BC BREAK] **Lazy transformation pipeline** — every source transformer is a deferred container service, tagged by the `SourceTransformer` interface and assembled in registration order; the stream filter and the transformers are only registered/constructed on the first cache miss (`SourceTransformingLoader::ensureRegistered()`), never on a warm-cache request. The protected `AspectKernel::registerTransformers()` hook (returning transformer instances, used e.g. by AspectMock to swap in its own weaver) is replaced by `AspectKernel::registerTransformerServices(AspectContainer $container)`, which registers deferred container definitions: override it to replace, omit, reorder or extend the built-in transformers, or simply `addLazyService()` an extra `SourceTransformer` service from `configureAop()` to append one. diff --git a/README.md b/README.md index cd421a94..ff3ce745 100644 --- a/README.md +++ b/README.md @@ -304,7 +304,9 @@ Interceptor::around(The::advice('advisor.Demo\Aspect\DynamicMethodsAspect->aroun ### 5. Register the aspect in the aspect kernel -To register the aspect just add an instance of it in the `configureAop()` method of the kernel: +An aspect is a typical container service. Add it in the `configureAop()` method of the +kernel, either eagerly as an instance or - preferably - as a deferred definition that is +only constructed when one of its advices actually runs: ```php registerAspect(new MonitorAspect()); + // Deferred (recommended): constructed on first use + $container->addLazyService(MonitorAspect::class, static fn() => new MonitorAspect()); + + // Eager alternative: $container->add(MonitorAspect::class, new MonitorAspect()); } //... diff --git a/demos/Demo/Aspect/AwesomeAspectKernel.php b/demos/Demo/Aspect/AwesomeAspectKernel.php index 21981f21..32f9edd0 100644 --- a/demos/Demo/Aspect/AwesomeAspectKernel.php +++ b/demos/Demo/Aspect/AwesomeAspectKernel.php @@ -25,13 +25,13 @@ class AwesomeAspectKernel extends AspectKernel */ protected function configureAop(AspectContainer $container): void { - $container->registerAspect(CachingAspect::class); - $container->registerAspect(LoggingAspect::class); - $container->registerAspect(IntroductionAspect::class); - $container->registerAspect(PropertyInterceptorAspect::class); - $container->registerAspect(FunctionInterceptorAspect::class); - $container->registerAspect(FluentInterfaceAspect::class); - $container->registerAspect(HealthyLiveAspect::class); - $container->registerAspect(DynamicMethodsAspect::class); + $container->addLazyService(CachingAspect::class, static fn(): CachingAspect => new CachingAspect()); + $container->addLazyService(LoggingAspect::class, static fn(): LoggingAspect => new LoggingAspect()); + $container->addLazyService(IntroductionAspect::class, static fn(): IntroductionAspect => new IntroductionAspect()); + $container->addLazyService(PropertyInterceptorAspect::class, static fn(): PropertyInterceptorAspect => new PropertyInterceptorAspect()); + $container->addLazyService(FunctionInterceptorAspect::class, static fn(): FunctionInterceptorAspect => new FunctionInterceptorAspect()); + $container->addLazyService(FluentInterfaceAspect::class, static fn(): FluentInterfaceAspect => new FluentInterfaceAspect()); + $container->addLazyService(HealthyLiveAspect::class, static fn(): HealthyLiveAspect => new HealthyLiveAspect()); + $container->addLazyService(DynamicMethodsAspect::class, static fn(): DynamicMethodsAspect => new DynamicMethodsAspect()); } } diff --git a/src/Aop/AGENTS.md b/src/Aop/AGENTS.md index 89ccb3fc..6845cc68 100644 --- a/src/Aop/AGENTS.md +++ b/src/Aop/AGENTS.md @@ -37,7 +37,7 @@ Proxy generators use TypeGenerator::renderTypeForPhpDoc() to emit V as 2nd gener ## Advice wiring (src/Aop/Framework/) - The — proxy-code accessor: aspect(X::class) fetches aspect from container; advice('advisorId') resolves container-backed closure advice (unwraps Advisor/AbstractInterceptor to raw Closure) -- Interceptor — @internal factory facade with TWO construction modes: before()/after()/around()/afterThrowing(class-string|Closure, ?string $methodName=null, int $order=0, string $expression=''). With aspect class + method name it returns a native PHP 8.4 lazy proxy (newLazyProxy) — interceptor construction, The::aspect() resolution and FCC creation all defer until first real use (invocation/ordering), so unmatched advices never instantiate their aspect; ONLY compiled advisor cache files use this lazy form. A ready Closure constructs eagerly, and generated PROXY classes deliberately use the eager `The::aspect(X::class)->method(...)` FCC form (InterceptorListGenerator): the proxy method/hook is already executing, so the interceptor is needed right now and a lazy detour would be pure overhead; The::advice('') stays eager too. Free to change between releases +- Interceptor — @internal factory facade with TWO construction modes: before()/after()/around()/afterThrowing(class-string|Closure, ?string $methodName=null, int $order=0, string $expression=''). With aspect class + method name it returns a native PHP 8.4 lazy proxy (via Go\Core\NativeLazyProxy::create) — interceptor construction, The::aspect() resolution and FCC creation all defer until first real use (invocation/ordering), so unmatched advices never instantiate their aspect; ONLY compiled advisor cache files use this lazy form. A ready Closure constructs eagerly, and generated PROXY classes deliberately use the eager `The::aspect(X::class)->method(...)` FCC form (InterceptorListGenerator): the proxy method/hook is already executing, so the interceptor is needed right now and a lazy detour would be pure overhead; The::advice('') stays eager too. Free to change between releases - GeneratedInterceptor — internal descriptor built by AbstractJoinpoint::flatAndSortAdvices() via fromAdvice(); usesContainerAdvice=true when advice closure isn't scoped to an Aspect class - AdviceTypeEnum — Advice::getType() kind + sorting priority (before → after/afterThrowing → around → introduction); replaced AdviceBefore/AdviceAfter/AdviceAround marker interfaces - Advice methods MUST be public (FCC calls them on the aspect instance from generated code) diff --git a/src/Aop/Framework/Interceptor.php b/src/Aop/Framework/Interceptor.php index 92222097..10c79ceb 100644 --- a/src/Aop/Framework/Interceptor.php +++ b/src/Aop/Framework/Interceptor.php @@ -15,7 +15,7 @@ use Closure; use Go\Aop\Aspect; use Go\Aop\AspectException; -use ReflectionClass; +use Go\Core\NativeLazyProxy; /** * Factory facade for generated interceptor declarations, with two construction modes. @@ -93,7 +93,10 @@ private static function createLazily( throw new AspectException('Advice method name is required when an aspect class name is given'); } - return new ReflectionClass($interceptorClass)->newLazyProxy( + // Interceptor classes are framework-owned and lazy-compatible by construction, + // so they take the probe-free trusted path. + return NativeLazyProxy::create( + $interceptorClass, static fn(): AbstractInterceptor => new $interceptorClass( The::aspect($aspectClassOrAdvice)->$methodName(...), $order, diff --git a/src/Core/AGENTS.md b/src/Core/AGENTS.md index 5360f11c..25c69f6e 100644 --- a/src/Core/AGENTS.md +++ b/src/Core/AGENTS.md @@ -1,8 +1,10 @@ # src/Core — Container and aspect loading ## Container (Container.php) -- DI container: add(by class-string|key), getService(), addLazyService(Closure) -- Automatic tagging by interface +- Generic, AOP-unaware DI container: add(by class-string|key), getService(), getValue(), addLazyService(Closure), onRegistration(interfaceFQCN, listener), addResource() +- Automatic tagging by interface; deferred services materialize as native lazy proxies via NativeLazyProxy (engine probe, no userland compatibility predicate) +- Aspects are typical services — no registerAspect(); the kernel arms a debug-only Aspect::class onRegistration listener for resource tracking and registers framework services via FrameworkServices::register() +- Throws SPL exceptions only (InvalidArgumentException, UnexpectedValueException, OutOfBoundsException) ## Aspect loading - AspectLoader — scans aspect classes for pointcut/advice attributes → Advisor[]; CachedAspectLoader decorates it (both implement AspectLoaderInterface) diff --git a/src/Core/AspectContainer.php b/src/Core/AspectContainer.php index 7c76ccf1..49838607 100644 --- a/src/Core/AspectContainer.php +++ b/src/Core/AspectContainer.php @@ -14,7 +14,6 @@ use Closure; use OutOfBoundsException; -use Go\Aop\Aspect; /** * Aspect container interface @@ -109,22 +108,20 @@ public function getValue(string $key): mixed; public function has(string $id): bool; /** - * Register an aspect in the container + * Registers a listener that is called whenever a deferred service whose id is a + * subclass of the given interface is added via {@see addLazyService()}. * - * Passing an aspect instance registers it immediately, exactly as before. + * The listener receives the service id (class-name) and the container - never the + * service value, so laziness of the registered services is fully preserved. Matching + * ids are autoloaded by the is_subclass_of() probe, which is the accepted cost of + * arming a listener; with no listeners registered, registration stays autoload-free. + * Eagerly added instances ({@see add()}) do not fire listeners - they are already + * tagged by their interfaces and tracked as resources at addition time. * - * Passing a class-name defers construction until the aspect is first needed (first - * advice hit, or first real interaction with the lazy object handed out during aspect - * enumeration on the weaving path), keeping it off the hot boot path. - * An aspect with required constructor arguments must also pass a factory closure that - * creates the instance; without a factory the class must be default-constructible, - * which is validated when the aspect materializes into its lazy object. - * - * @param Aspect|class-string $aspectOrClassName Aspect instance or its class-name - * @param null|Closure(AspectContainer $container): Aspect $aspectFactory Factory for deferred - * construction (only allowed together with a class-name) + * @param class-string $interfaceName Interface the deferred service ids are matched against + * @param Closure(class-string $id, AspectContainer $container): void $listener */ - public function registerAspect(Aspect|string $aspectOrClassName, ?Closure $aspectFactory = null): void; + public function onRegistration(string $interfaceName, Closure $listener): void; /** * Checks if all tracked file resources are still fresh at the given timestamp @@ -158,4 +155,15 @@ public function add(string $id, mixed $value): void; * @template T of object */ public function addLazyService(string $id, Closure $lazyInitializationClosure): void; + + /** + * Adds a link to the file resource into the container + * + * This set of resources is used later to check the freshness of cache + * + * @internal Used by the framework itself (e.g. debug-mode aspect source tracking) + * + * @param string $resource Path to the resource + */ + public function addResource(string $resource): void; } diff --git a/src/Core/AspectKernel.php b/src/Core/AspectKernel.php index 26ac2aee..2ab42f45 100644 --- a/src/Core/AspectKernel.php +++ b/src/Core/AspectKernel.php @@ -12,6 +12,7 @@ namespace Go\Core; +use Go\Aop\Aspect; use Go\Aop\AspectException; use Go\Aop\Features; use Go\Core\Cache\CachedAspectLoader; @@ -23,6 +24,7 @@ use Go\Instrument\Transformer\FilterInjectorTransformer; use Go\Instrument\Transformer\MagicConstantTransformer; use Go\Instrument\Transformer\WeavingTransformer; +use ReflectionClass; use RuntimeException; use function define; @@ -137,6 +139,10 @@ public function init(array $options = []): void $container->add('kernel.interceptFunctions', $this->hasFeature(Features::INTERCEPT_FUNCTIONS)); $container->add('kernel.options', $this->options); + // The framework's own services are deferred definitions registered through the + // generic lazy container API - the container itself knows nothing about them. + FrameworkServices::register($container); + // The whole transformer pipeline (and the stream filter itself) is only needed on // a cache miss, so every transformer is registered as a typical deferred container // service and brought up by SourceTransformingLoader::ensureRegistered() from the @@ -147,6 +153,21 @@ public function init(array $options = []): void AopComposerLoader::init($this->options, $container); + // In debug mode every lazily registered aspect's source file must be tracked as a + // resource right away: SourceTransformingLoader consults resource freshness before + // any aspect materializes. Production arms no listener - registration stays a pure + // array write, its warm path never checks freshness, and a cache miss materializes + // every aspect during weaving anyway. Armed after the framework/transformer + // services above, so only aspects from configureAop() pass through it. + if ($this->options['debug']) { + $container->onRegistration(Aspect::class, static function (string $aspectClassName) use ($container): void { + $aspectFileName = (new ReflectionClass($aspectClassName))->getFileName(); + if (is_string($aspectFileName)) { + $container->addResource($aspectFileName); + } + }); + } + // Register all AOP configuration in the container $this->configureAop($container); diff --git a/src/Core/Container.php b/src/Core/Container.php index 1d94b3b3..0739d5b2 100644 --- a/src/Core/Container.php +++ b/src/Core/Container.php @@ -13,16 +13,10 @@ namespace Go\Core; use Closure; -use Go\Aop\Aspect; -use Go\Aop\AspectException; -use Go\Aop\Pointcut\PointcutGrammar; -use Go\Core\Cache\CachedAspectLoader; -use Go\Aop\Pointcut\PointcutLexer; -use Go\Aop\Pointcut\PointcutParser; -use Go\Instrument\ClassLoading\CachePathManager; +use InvalidArgumentException; use OutOfBoundsException; -use ReflectionClass; use ReflectionObject; +use UnexpectedValueException; /** * DI-container @@ -40,10 +34,10 @@ class Container implements AspectContainer private array $factories = []; /** - * @var array Optional eager validators for deferred services, run when the - * lazy object is created (first retrieval) - before the factory itself runs (first actual use) + * @var array> Registration listeners, + * keyed by the interface their deferred service ids are matched against */ - private array $factoryValidators = []; + private array $registrationListeners = []; /** * @var array> Holds information about mapping of interface tags into identifiers @@ -68,139 +62,11 @@ class Container implements AspectContainer public function __construct(array $resources = []) { $this->resources = array_combine($resources, $resources); - - $this->addLazyService(PointcutLexer::class, fn(): PointcutLexer => new PointcutLexer()); - - $this->addLazyService(PointcutParser::class, fn(): PointcutParser => new PointcutParser( - new PointcutGrammar(), - )); - - $this->addLazyService(AdviceMatcher::class, fn(AspectContainer $container): AdviceMatcher => new AdviceMatcher( - (bool) $container->getValue('kernel.interceptFunctions'), - )); - - $this->addLazyService(AttributeAspectLoaderExtension::class, fn(AspectContainer $container): AttributeAspectLoaderExtension => new AttributeAspectLoaderExtension( - $container->getService(PointcutLexer::class), - $container->getService(PointcutParser::class), - )); - - $this->addLazyService(IntroductionAspectExtension::class, fn(AspectContainer $container): IntroductionAspectExtension => new IntroductionAspectExtension( - $container->getService(PointcutLexer::class), - $container->getService(PointcutParser::class), - )); - - $this->addLazyService(AspectLoader::class, fn(AspectContainer $container): AspectLoader => new AspectLoader( - $container, - $container->getService(AttributeAspectLoaderExtension::class), - $container->getService(IntroductionAspectExtension::class), - )); - - $this->addLazyService(CachedAspectLoader::class, function (AspectContainer $container): CachedAspectLoader { - $options = $container->getService(AspectKernel::class)->getOptions(); - - return new CachedAspectLoader($container, AspectLoader::class, $options); - }); - - $this->addLazyService(CachePathManager::class, fn(AspectContainer $container): CachePathManager => new CachePathManager( - $container->getService(AspectKernel::class), - )); - } - - final public function registerAspect(Aspect|string $aspectOrClassName, ?Closure $aspectFactory = null): void - { - if ($aspectOrClassName instanceof Aspect) { - $this->add($aspectOrClassName::class, $aspectOrClassName); - - return; - } - - // Deferred registration by class-name: the aspect is constructed on first use - // (first advice hit, or first real interaction with the lazy object handed out - // on the weaving path), so a hot-cache request never pays for aspects it does - // not touch. - $this->addLazyService($aspectOrClassName, function () use ($aspectOrClassName, $aspectFactory): Aspect { - return $this->materializeAspect($aspectOrClassName, $aspectFactory); - }); - - // Cheap aspect declaration checks (implements Aspect, constructibility) run as soon - // as the service materializes into a lazy object, so misconfiguration surfaces on - // retrieval - construction itself stays deferred until first actual use. - $this->factoryValidators[$aspectOrClassName] = function () use ($aspectOrClassName, $aspectFactory): void { - $this->validateAspectRegistration($aspectOrClassName, $aspectFactory); - }; - - // In debug mode the aspect's source file must be tracked as a resource right away: - // SourceTransformingLoader consults resource freshness before any aspect materializes. - // Production skips this - its warm path never checks freshness, and a cache miss - // materializes every aspect during weaving anyway. - if ($this->isDebug()) { - if (!is_subclass_of($aspectOrClassName, Aspect::class)) { - throw new AspectException("Aspect class $aspectOrClassName must implement " . Aspect::class); - } - $aspectFileName = (new ReflectionClass($aspectOrClassName))->getFileName(); - if (is_string($aspectFileName)) { - $this->addResource($aspectFileName); - } - } - } - - /** - * Constructs a lazily registered aspect, either through its factory or by validated - * default construction - * - * @param null|Closure(AspectContainer): Aspect $aspectFactory - */ - private function materializeAspect(string $aspectClassName, ?Closure $aspectFactory): Aspect - { - $this->validateAspectRegistration($aspectClassName, $aspectFactory); - assert(is_subclass_of($aspectClassName, Aspect::class)); - - if ($aspectFactory !== null) { - $aspect = $aspectFactory($this); - if (!$aspect instanceof $aspectClassName) { - throw new AspectException("Aspect factory for $aspectClassName returned an incompatible object"); - } - - return $aspect; - } - - return new $aspectClassName(); - } - - /** - * Validates a deferred aspect registration without constructing the aspect - * - * @param null|Closure(AspectContainer): Aspect $aspectFactory - * - * @throws AspectException if the class is not an aspect or cannot be default-constructed - */ - private function validateAspectRegistration(string $aspectClassName, ?Closure $aspectFactory): void - { - if (!is_subclass_of($aspectClassName, Aspect::class)) { - throw new AspectException("Aspect class $aspectClassName must implement " . Aspect::class); - } - if ($aspectFactory === null) { - $constructor = (new ReflectionClass($aspectClassName))->getConstructor(); - if ($constructor !== null && $constructor->getNumberOfRequiredParameters() > 0) { - throw new AspectException( - "Aspect $aspectClassName has required constructor arguments, " - . "pass a factory closure to registerAspect() to create it", - ); - } - } } - /** - * Whether the kernel that owns this container runs in debug mode - */ - private function isDebug(): bool + final public function onRegistration(string $interfaceName, Closure $listener): void { - if (!$this->has('kernel.options')) { - return false; - } - $options = $this->getValue('kernel.options'); - - return is_array($options) && ($options['debug'] ?? false) === true; + $this->registrationListeners[$interfaceName][] = $listener; } final public function add(string $id, mixed $value): void @@ -225,25 +91,30 @@ final public function addLazyService(string $id, Closure $lazyInitializationClos // keys with is_subclass_of(), so an arbitrary string id must be rejected upfront // (checked syntactically to avoid autoloading anything at registration time). if (preg_match('/^\\\\?[A-Za-z_\x80-\xff][\w\x80-\xff]*(\\\\[A-Za-z_\x80-\xff][\w\x80-\xff]*)*$/', $id) !== 1) { - throw new AspectException("Lazy service id must be a valid class name, \"$id\" given"); + throw new InvalidArgumentException("Lazy service id must be a valid class name, \"$id\" given"); } $this->factories[$id] = $lazyInitializationClosure; - unset($this->factoryValidators[$id]); + + // With no listeners registered (the production configuration) this is a no-op and + // nothing below autoloads; a registered listener accepts the is_subclass_of() + // autoload of matching ids as its cost. + foreach ($this->registrationListeners as $interfaceName => $listeners) { + if (is_subclass_of($id, $interfaceName)) { + foreach ($listeners as $listener) { + $listener($id, $this); + } + } + } } final public function getService(string $className): object { - if (!isset($this->values[$className]) && isset($this->factories[$className])) { - $this->materializeService($className); - } - if (!isset($this->values[$className])) { - throw new OutOfBoundsException("Value $className is not defined in the container"); - } - if (!$this->values[$className] instanceof $className) { - throw new AspectException("Service $className is not properly registered"); + $service = $this->getValue($className); + if (!$service instanceof $className) { + throw new UnexpectedValueException("Service $className is not properly registered"); } - return $this->values[$className]; + return $service; } final public function getValue(string $key): mixed @@ -291,11 +162,10 @@ final public function getServicesByInterface(string $interfaceTagClassName): arr * Materializes a deferred service into a container entry and tags it by its interfaces. * * Where the class supports it, the entry becomes a native lazy proxy - * ({@see ReflectionClass::newLazyProxy()}): a typed, instanceof-correct instance of the - * service class whose factory only runs on first actual interaction with the object. - * Classes that PHP cannot make lazy (internal classes and their non-stdClass subclasses, - * abstract classes, enums, readonly classes before PHP 8.5) and ids that are not loadable - * classes fall back to invoking the factory eagerly, as before. + * ({@see NativeLazyProxy}): a typed, instanceof-correct instance of the service class + * whose factory only runs on first actual interaction with the object. Classes that + * PHP cannot make lazy and ids that are not loadable classes fall back to invoking + * the factory eagerly, as before. */ private function materializeService(string $id): void { @@ -308,10 +178,6 @@ private function materializeService(string $id): void // eager fallback factory can re-enter the container and must not materialize $id twice unset($this->factories[$id]); - $validator = $this->factoryValidators[$id] ?? null; - unset($this->factoryValidators[$id]); - $validator?->__invoke(); - $this->add($id, $this->createLazyService($id, $factory)); } @@ -326,56 +192,15 @@ private function createLazyService(string $id, Closure $factory): object if (!class_exists($id)) { return $factory($this); } - $reflection = new ReflectionClass($id); - if (!self::isLazyProxyCompatible($reflection)) { - return $factory($this); - } - return $reflection->newLazyProxy(function () use ($id, $factory): object { + return NativeLazyProxy::tryCreate($id, function () use ($id, $factory): object { $instance = $factory($this); if (!$instance instanceof $id) { - throw new AspectException("Service $id is not properly registered"); + throw new UnexpectedValueException("Service $id is not properly registered"); } return $instance; - }); - } - - /** - * Whether PHP can create a native lazy proxy for the given class - * - * @param ReflectionClass $reflection - */ - private static function isLazyProxyCompatible(ReflectionClass $reflection): bool - { - if ($reflection->isInternal() || $reflection->isAbstract() || $reflection->isEnum()) { - return false; - } - // Lazy objects for readonly classes are only supported since PHP 8.5 - if (PHP_VERSION_ID < 80500 && $reflection->isReadOnly()) { - return false; - } - // PHP creates lazy objects of classes without instance properties as already - // initialized, so the initializer (and with it the service factory) would never - // run - such services keep the eager construction path - $hasInstanceProperties = false; - foreach ($reflection->getProperties() as $property) { - if (!$property->isStatic() && !$property->isVirtual()) { - $hasInstanceProperties = true; - break; - } - } - if (!$hasInstanceProperties) { - return false; - } - // Subclasses of internal classes (other than stdClass) cannot be lazy - for ($parent = $reflection->getParentClass(); $parent !== false; $parent = $parent->getParentClass()) { - if ($parent->isInternal()) { - return $parent->getName() === 'stdClass'; - } - } - - return true; + }) ?? $factory($this); } final public function isFreshSince(int $timestamp): bool @@ -387,14 +212,7 @@ final public function isFreshSince(int $timestamp): bool return $this->cachedMaxTimestamp <= $timestamp; } - /** - * Adds a link to the file resource into the container - * - * This set of resources is used later to check the freshness of cache - * - * @param string $resource Path to the resource - */ - final protected function addResource(string $resource): void + final public function addResource(string $resource): void { if (!isset($this->resources[$resource]) && is_readable($resource)) { $this->resources[$resource] = $resource; diff --git a/src/Core/FrameworkServices.php b/src/Core/FrameworkServices.php new file mode 100644 index 00000000..73f30351 --- /dev/null +++ b/src/Core/FrameworkServices.php @@ -0,0 +1,68 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Core; + +use Go\Aop\Pointcut\PointcutGrammar; +use Go\Aop\Pointcut\PointcutLexer; +use Go\Aop\Pointcut\PointcutParser; +use Go\Core\Cache\CachedAspectLoader; +use Go\Instrument\ClassLoading\CachePathManager; + +/** + * Deferred definitions of the framework's own services, registered by the kernel during + * initialization through the generic lazy container API. + * + * Lives outside the container on purpose: the container is a generic DI implementation + * with no knowledge of the AOP services it happens to hold. + */ +final class FrameworkServices +{ + public static function register(AspectContainer $container): void + { + $container->addLazyService(PointcutLexer::class, fn(): PointcutLexer => new PointcutLexer()); + + $container->addLazyService(PointcutParser::class, fn(): PointcutParser => new PointcutParser( + new PointcutGrammar(), + )); + + $container->addLazyService(AdviceMatcher::class, fn(AspectContainer $container): AdviceMatcher => new AdviceMatcher( + (bool) $container->getValue('kernel.interceptFunctions'), + )); + + $container->addLazyService(AttributeAspectLoaderExtension::class, fn(AspectContainer $container): AttributeAspectLoaderExtension => new AttributeAspectLoaderExtension( + $container->getService(PointcutLexer::class), + $container->getService(PointcutParser::class), + )); + + $container->addLazyService(IntroductionAspectExtension::class, fn(AspectContainer $container): IntroductionAspectExtension => new IntroductionAspectExtension( + $container->getService(PointcutLexer::class), + $container->getService(PointcutParser::class), + )); + + $container->addLazyService(AspectLoader::class, fn(AspectContainer $container): AspectLoader => new AspectLoader( + $container, + $container->getService(AttributeAspectLoaderExtension::class), + $container->getService(IntroductionAspectExtension::class), + )); + + $container->addLazyService(CachedAspectLoader::class, function (AspectContainer $container): CachedAspectLoader { + $options = $container->getService(AspectKernel::class)->getOptions(); + + return new CachedAspectLoader($container, AspectLoader::class, $options); + }); + + $container->addLazyService(CachePathManager::class, fn(AspectContainer $container): CachePathManager => new CachePathManager( + $container->getService(AspectKernel::class), + )); + } +} diff --git a/src/Core/NativeLazyProxy.php b/src/Core/NativeLazyProxy.php new file mode 100644 index 00000000..22a604b9 --- /dev/null +++ b/src/Core/NativeLazyProxy.php @@ -0,0 +1,91 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Core; + +use Closure; +use Error; +use ReflectionClass; +use ReflectionException; + +/** + * Creates native lazy proxies ({@see ReflectionClass::newLazyProxy()}) through a single + * probe-based compatibility path shared by the container and generated interceptor code. + * + * Instead of re-deriving in userland which classes PHP can make lazy (internal classes + * and their non-stdClass subclasses, abstract classes, enums - a set that shifts + * between engine versions), the engine itself is asked: an incompatible class makes + * newLazyProxy() throw. The one silent case - classes without instance properties, whose proxies come + * back already initialized so the initializer (and with it the service factory) would + * never run - is detected with a single isUninitializedLazyObject() call on the created + * proxy. In the common compatible case the whole check costs one engine-level boolean. + */ +final class NativeLazyProxy +{ + /** + * @var array Classes PHP refused to make lazy, so long-running + * workers skip the throwing attempt on repeated registrations + */ + private static array $unsupported = []; + + /** + * Creates a lazy proxy of the class, or returns null when PHP cannot make the class + * lazy - the caller is expected to fall back to eager construction + * + * @template T of object + * + * @param class-string $className + * @param Closure(T): T $initializer Factory of the real instance, invoked on first + * actual interaction with the proxy + * + * @return T|null + */ + public static function tryCreate(string $className, Closure $initializer): ?object + { + if (isset(self::$unsupported[$className])) { + return null; + } + $reflection = new ReflectionClass($className); + try { + $proxy = $reflection->newLazyProxy($initializer); + } catch (ReflectionException|Error) { + self::$unsupported[$className] = true; + + return null; + } + // PHP creates lazy objects of classes without instance properties as already + // initialized, so the initializer would never run for them + if (!$reflection->isUninitializedLazyObject($proxy)) { + self::$unsupported[$className] = true; + + return null; + } + + return $proxy; + } + + /** + * Creates a lazy proxy of a class known to be proxy-compatible by construction + * (framework-owned classes), skipping the probe and the memo + * + * @template T of object + * + * @param class-string $className + * @param Closure(T): T $initializer + * + * @return T + */ + public static function create(string $className, Closure $initializer): object + { + return new ReflectionClass($className)->newLazyProxy($initializer); + } +} diff --git a/tests/Aop/Framework/InterceptorTest.php b/tests/Aop/Framework/InterceptorTest.php index ac839fd1..701243ff 100644 --- a/tests/Aop/Framework/InterceptorTest.php +++ b/tests/Aop/Framework/InterceptorTest.php @@ -31,7 +31,7 @@ private function initKernelWithTestAspect(): void { $kernel = InterceptorTestAspectKernel::getInstance(); $container = new Container(); - $container->registerAspect(new InterceptorTestAspect()); + $container->add(InterceptorTestAspect::class, new InterceptorTestAspect()); $containerProperty = new ReflectionProperty(AspectKernel::class, 'container'); $containerProperty->setValue($kernel, $container); @@ -116,7 +116,7 @@ final class InterceptorTestAspectKernel extends AspectKernel { protected function configureAop(AspectContainer $container): void { - $container->registerAspect(new InterceptorTestAspect()); + $container->add(InterceptorTestAspect::class, new InterceptorTestAspect()); } } diff --git a/tests/Aop/Framework/TheTest.php b/tests/Aop/Framework/TheTest.php index 4672666a..99e8697f 100644 --- a/tests/Aop/Framework/TheTest.php +++ b/tests/Aop/Framework/TheTest.php @@ -143,7 +143,7 @@ private function initKernelWithContainerValues(array $values): void { $kernel = TheTestAspectKernel::getInstance(); $container = new Container(); - $container->registerAspect(new TheTestAspect()); + $container->add(TheTestAspect::class, new TheTestAspect()); foreach ($values as $id => $value) { $container->add($id, $value); } @@ -157,7 +157,7 @@ final class TheTestAspectKernel extends AspectKernel { protected function configureAop(AspectContainer $container): void { - $container->registerAspect(new TheTestAspect()); + $container->add(TheTestAspect::class, new TheTestAspect()); } } diff --git a/tests/Core/Cache/AdvisorCacheCompilerTest.php b/tests/Core/Cache/AdvisorCacheCompilerTest.php index 9fce17be..cfccc53a 100644 --- a/tests/Core/Cache/AdvisorCacheCompilerTest.php +++ b/tests/Core/Cache/AdvisorCacheCompilerTest.php @@ -31,6 +31,7 @@ use Go\Aop\Support\GenericPointcutAdvisor; use Go\Aop\Support\LazyPointcutAdvisor; use Go\Core\Container; +use Go\Core\FrameworkServices; use Go\Tests\TestProject\Annotation\Loggable; use Go\Tests\TestProject\Application\BehaviorTrait; use Go\Tests\TestProject\Application\FooInterface; @@ -168,8 +169,10 @@ public function testCompilesLazyPointcutAdvisorToResolvedGenericAdvisor(): void { $aspect = new DoSomethingAspect(); $adviceClosure = new ReflectionMethod(DoSomethingAspect::class, 'afterDoSomething')->getClosure($aspect); + $container = new Container(); + FrameworkServices::register($container); $lazyAdvisor = new LazyPointcutAdvisor( - new Container(), + $container, 'execution(public Go\Tests\TestProject\Application\*->doSomething(*))', new AfterInterceptor($adviceClosure), ); diff --git a/tests/Core/ContainerTest.php b/tests/Core/ContainerTest.php index 3113ced6..72b3101a 100644 --- a/tests/Core/ContainerTest.php +++ b/tests/Core/ContainerTest.php @@ -14,7 +14,6 @@ use Go\Aop\Advisor; use Go\Aop\Aspect; -use Go\Aop\AspectException; use Go\Aop\Pointcut; use Go\Aop\Pointcut\PointcutLexer; use Go\Aop\Pointcut\PointcutParser; @@ -22,9 +21,7 @@ use Go\Stubs\First; use Go\Tests\TestProject\Aspect\DoSomethingAspect; use Go\Tests\TestProject\Aspect\EnumMethodAspect; -use Go\Tests\TestProject\Aspect\LoggingAspect; use PHPUnit\Framework\TestCase; -use Psr\Log\NullLogger; use stdClass; #[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] @@ -49,6 +46,7 @@ protected function setUp(): void $this->container->add(AspectKernel::class, $mockKernel); $this->container->add('kernel.options', ['cacheDir' => '/tmp']); $this->container->add('kernel.interceptFunctions', false); + FrameworkServices::register($this->container); } /** @@ -107,14 +105,14 @@ public function testAdvisorCanBeRegistered(): void } /** - * Tests that aspect can be registered and accessed + * Tests that an aspect registered as a plain eager service is tagged as an aspect */ public function testAspectCanBeRegisteredAndReceived(): void { $aspect = $this->createMock(Aspect::class); $aspectClass = $aspect::class; - $this->container->registerAspect($aspect); + $this->container->add($aspectClass, $aspect); $this->assertSame($aspect, $this->container->getService($aspectClass)); // Verify that tag is working @@ -170,7 +168,7 @@ public function testGetValueThrowsOutOfBoundsExceptionOnUnknown(): void public function testGetServiceEnsuresThatKeyAndReturnedTypeMatches(): void { - $this->expectException(AspectException::class); + $this->expectException(\UnexpectedValueException::class); $this->expectExceptionMessage('Service ' . First::class . ' is not properly registered'); // Emulation of incorrect types @@ -216,43 +214,54 @@ public function testLazyServiceIsTaggedByInterface(): void public function testLazyServiceRejectsNonClassNameId(): void { - $this->expectException(AspectException::class); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessageMatches('/Lazy service id must be a valid class name/'); // @phpstan-ignore argument.type (the invalid service id is the test subject) $this->container->addLazyService('kernel.not-a-class', fn(): PointcutLexer => new PointcutLexer()); } + public function testLazyServiceFactoryReturningWrongTypeFailsOnFirstUse(): void + { + $this->container->addLazyService(StatefulTestAspect::class, fn(): object => new stdClass()); + + $aspect = $this->container->getService(StatefulTestAspect::class); + + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('Service ' . StatefulTestAspect::class . ' is not properly registered'); + (new \ReflectionClass(StatefulTestAspect::class))->initializeLazyObject($aspect); + } + public function testAspectRegisteredByClassNameIsConstructedOnFirstUse(): void { $constructed = false; - $this->container->registerAspect( - LoggingAspect::class, - function () use (&$constructed): LoggingAspect { + $this->container->addLazyService( + StatefulTestAspect::class, + function () use (&$constructed): StatefulTestAspect { $constructed = true; - return new LoggingAspect(new NullLogger()); + return new StatefulTestAspect(42); }, ); - $this->assertTrue($this->container->has(LoggingAspect::class)); + $this->assertTrue($this->container->has(StatefulTestAspect::class)); $this->assertFalse($constructed, 'Aspect should not have been constructed at registration'); // Retrieval returns an instanceof-correct lazy object, construction is still deferred - $aspect = $this->container->getService(LoggingAspect::class); + $aspect = $this->container->getService(StatefulTestAspect::class); // @phpstan-ignore method.alreadyNarrowedType (runtime double-check that the lazy proxy reports the right class) - $this->assertInstanceOf(LoggingAspect::class, $aspect); + $this->assertInstanceOf(StatefulTestAspect::class, $aspect); // @phpstan-ignore method.alreadyNarrowedType (the flag can be flipped by reference inside the service factory) $this->assertFalse($constructed, 'Aspect should not have been constructed by retrieval'); // First real interaction with the aspect object triggers the factory - (new \ReflectionClass(LoggingAspect::class))->initializeLazyObject($aspect); + (new \ReflectionClass(StatefulTestAspect::class))->initializeLazyObject($aspect); // @phpstan-ignore method.impossibleType (the flag is flipped by reference inside the service factory) $this->assertTrue($constructed); } public function testLazyAspectAppearsInAspectInterfaceQuery(): void { - $this->container->registerAspect(DoSomethingAspect::class); + $this->container->addLazyService(DoSomethingAspect::class, fn(): DoSomethingAspect => new DoSomethingAspect()); $aspects = $this->container->getServicesByInterface(Aspect::class); $this->assertArrayHasKey(DoSomethingAspect::class, $aspects); @@ -262,7 +271,7 @@ public function testLazyAspectAppearsInAspectInterfaceQuery(): void public function testLazyAspectEnumerationHandsOutUninitializedLazyObjects(): void { $constructed = false; - $this->container->registerAspect( + $this->container->addLazyService( StatefulTestAspect::class, function () use (&$constructed): StatefulTestAspect { $constructed = true; @@ -293,7 +302,7 @@ public function testPropertylessServiceFallsBackToEagerConstruction(): void // PHP creates lazy objects of property-less classes as already initialized, // which would silently skip the factory - the container must construct these eagerly $constructed = false; - $this->container->registerAspect( + $this->container->addLazyService( DoSomethingAspect::class, function () use (&$constructed): DoSomethingAspect { $constructed = true; @@ -308,32 +317,12 @@ function () use (&$constructed): DoSomethingAspect { $this->assertTrue($constructed, 'Factory of a property-less service must run at materialization'); } - public function testLazyAspectWithRequiredConstructorArgsNeedsFactory(): void - { - // LoggingAspect requires a LoggerInterface constructor argument - $this->container->registerAspect(LoggingAspect::class); - - $this->expectException(AspectException::class); - $this->expectExceptionMessageMatches('/pass a factory closure/'); - $this->container->getService(LoggingAspect::class); - } - - public function testLazyAspectMustImplementAspectInterface(): void - { - // @phpstan-ignore argument.type (a non-aspect class is the test subject) - $this->container->registerAspect(stdClass::class); - - $this->expectException(AspectException::class); - $this->expectExceptionMessageMatches('/must implement/'); - $this->container->getService(stdClass::class); - } - public function testReRegisteringPendingFactoryKeepsTagOrderAndReplacesFactory(): void { // Downstream kernels replace built-in pipeline services by re-registering the // same id; the replacement must keep the id's position in the chain order - $this->container->registerAspect(DoSomethingAspect::class); - $this->container->registerAspect(EnumMethodAspect::class); + $this->container->addLazyService(DoSomethingAspect::class, fn(): DoSomethingAspect => new DoSomethingAspect()); + $this->container->addLazyService(EnumMethodAspect::class, fn(): EnumMethodAspect => new EnumMethodAspect()); $replaced = false; $this->container->addLazyService(DoSomethingAspect::class, function () use (&$replaced): DoSomethingAspect { @@ -354,7 +343,7 @@ public function testInterfaceQuerySurvivesReentrantMaterialization(): void // A factory that re-enters getServicesByInterface() consumes other pending // factories from under the outer materialization loop (this also happens // implicitly when an aspect class is autoloaded through the weaving pipeline) - $this->container->registerAspect( + $this->container->addLazyService( DoSomethingAspect::class, function (AspectContainer $container): DoSomethingAspect { $container->getServicesByInterface(Aspect::class); @@ -362,12 +351,54 @@ function (AspectContainer $container): DoSomethingAspect { return new DoSomethingAspect(); }, ); - $this->container->registerAspect(EnumMethodAspect::class); + $this->container->addLazyService(EnumMethodAspect::class, fn(): EnumMethodAspect => new EnumMethodAspect()); $aspects = $this->container->getServicesByInterface(Aspect::class); $this->assertArrayHasKey(DoSomethingAspect::class, $aspects); $this->assertArrayHasKey(EnumMethodAspect::class, $aspects); } + + public function testRegistrationListenerFiresForMatchingLazyIdsOnly(): void + { + $seen = []; + $this->container->onRegistration(Aspect::class, function (string $id, AspectContainer $container) use (&$seen): void { + $seen[] = $id; + $this->assertSame($this->container, $container); + }); + + $constructed = false; + $this->container->addLazyService(StatefulTestAspect::class, function () use (&$constructed): StatefulTestAspect { + $constructed = true; + + return new StatefulTestAspect(1); + }); + // A non-aspect deferred service must not fire the aspect listener + $this->container->addLazyService(PointcutLexer::class, fn(): PointcutLexer => new PointcutLexer()); + + $this->assertSame([StatefulTestAspect::class], $seen); + // The listener operates on ids only - the factory must not have run + $this->assertFalse($constructed, 'Registration listener must not defeat laziness'); + } + + public function testRegistrationListenerEnablesDebugResourceTracking(): void + { + // Emulates the debug-mode listener armed by AspectKernel::init(): every lazily + // registered aspect's source file becomes a tracked resource at registration time + $this->container->onRegistration(Aspect::class, function (string $id, AspectContainer $container): void { + $fileName = (new \ReflectionClass($id))->getFileName(); + if (is_string($fileName)) { + $container->addResource($fileName); + } + }); + + $this->container->addLazyService(DoSomethingAspect::class, fn(): DoSomethingAspect => new DoSomethingAspect()); + + $fileName = (new \ReflectionClass(DoSomethingAspect::class))->getFileName(); + $this->assertNotFalse($fileName); + $realMtime = filemtime($fileName); + $this->assertNotFalse($realMtime); + $this->assertFalse($this->container->isFreshSince($realMtime - 3600), 'Aspect file must be tracked before materialization'); + } } /** diff --git a/tests/Core/NativeLazyProxyTest.php b/tests/Core/NativeLazyProxyTest.php new file mode 100644 index 00000000..020f711b --- /dev/null +++ b/tests/Core/NativeLazyProxyTest.php @@ -0,0 +1,133 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Core; + +use ArrayObject; +use PHPUnit\Framework\TestCase; +use ReflectionClass; + +class NativeLazyProxyTest extends TestCase +{ + public function testCreatesUninitializedProxyForCompatibleClass(): void + { + $constructed = false; + $proxy = NativeLazyProxy::tryCreate(LazyProxyFixture::class, function () use (&$constructed): LazyProxyFixture { + $constructed = true; + + return new LazyProxyFixture(42); + }); + + $this->assertInstanceOf(LazyProxyFixture::class, $proxy); + $this->assertTrue((new ReflectionClass(LazyProxyFixture::class))->isUninitializedLazyObject($proxy)); + $this->assertFalse($constructed, 'Initializer must not run at proxy creation'); + + // First real interaction runs the initializer + $this->assertSame(42, $proxy->getValue()); + // @phpstan-ignore method.impossibleType (the flag is flipped by reference inside the initializer) + $this->assertTrue($constructed); + } + + public function testRefusesPropertylessClass(): void + { + // PHP creates lazy objects of property-less classes as already initialized, + // so the initializer would never run - such classes must be reported unsupported + $proxy = NativeLazyProxy::tryCreate(PropertylessFixture::class, fn(): PropertylessFixture => new PropertylessFixture()); + + $this->assertNull($proxy); + // Repeated attempts take the memoized path and stay refused + $this->assertNull(NativeLazyProxy::tryCreate(PropertylessFixture::class, fn(): PropertylessFixture => new PropertylessFixture())); + } + + public function testRefusesAbstractClass(): void + { + $this->assertNull(NativeLazyProxy::tryCreate(AbstractLazyFixture::class, fn(): object => new LazyProxyFixture(1))); + } + + public function testRefusesEnum(): void + { + $this->assertNull(NativeLazyProxy::tryCreate(LazyEnumFixture::class, fn(): object => LazyEnumFixture::One)); + } + + public function testRefusesInternalClass(): void + { + $this->assertNull(NativeLazyProxy::tryCreate(ArrayObject::class, fn(): ArrayObject => new ArrayObject())); + } + + public function testRefusesSubclassOfInternalClass(): void + { + $this->assertNull(NativeLazyProxy::tryCreate(InternalSubclassFixture::class, fn(): InternalSubclassFixture => new InternalSubclassFixture())); + } + + public function testReadonlyClassSupportFollowsEngineCapabilities(): void + { + // Current engines (PHP 8.4.19+, 8.5) can make readonly classes lazy; the probe + // asks the engine instead of hardcoding a version cutoff, so an engine without + // that support would simply get the eager fallback (null) here + $proxy = NativeLazyProxy::tryCreate(ReadonlyLazyFixture::class, fn(): ReadonlyLazyFixture => new ReadonlyLazyFixture(1)); + + $this->assertInstanceOf(ReadonlyLazyFixture::class, $proxy); + $this->assertTrue((new ReflectionClass(ReadonlyLazyFixture::class))->isUninitializedLazyObject($proxy)); + $this->assertSame(1, $proxy->getValue()); + } + + public function testTrustedCreateSkipsProbe(): void + { + $proxy = NativeLazyProxy::create(LazyProxyFixture::class, static fn(): LazyProxyFixture => new LazyProxyFixture(7)); + + $this->assertTrue((new ReflectionClass(LazyProxyFixture::class))->isUninitializedLazyObject($proxy)); + $this->assertSame(7, $proxy->getValue()); + } +} + +class LazyProxyFixture +{ + public function __construct(private readonly int $value) {} + + public function getValue(): int + { + return $this->value; + } +} + +class PropertylessFixture +{ + public function doNothing(): void {} +} + +abstract class AbstractLazyFixture +{ + public int $property = 0; +} + +enum LazyEnumFixture +{ + case One; +} + +/** + * @extends ArrayObject + */ +class InternalSubclassFixture extends ArrayObject +{ + public int $property = 0; +} + +readonly class ReadonlyLazyFixture +{ + public function __construct(private int $value) {} + + public function getValue(): int + { + return $this->value; + } +} diff --git a/tests/Fixtures/project/src/Kernel/DefaultAspectKernel.php b/tests/Fixtures/project/src/Kernel/DefaultAspectKernel.php index 12a3dfca..47b133ec 100644 --- a/tests/Fixtures/project/src/Kernel/DefaultAspectKernel.php +++ b/tests/Fixtures/project/src/Kernel/DefaultAspectKernel.php @@ -24,15 +24,15 @@ class DefaultAspectKernel extends AspectKernel */ protected function configureAop(AspectContainer $container): void { - $container->registerAspect(LoggingAspect::class, fn(): LoggingAspect => new LoggingAspect(new NullLogger())); - $container->registerAspect(DoSomethingAspect::class); - $container->registerAspect(ArrayPropertyInterceptAspect::class); - $container->registerAspect(PropertyInterceptAspect::class); - $container->registerAspect(PromotedPropertyInterceptAspect::class); - $container->registerAspect(Issue293Aspect::class); - $container->registerAspect(InitializationAspect::class); - $container->registerAspect(WeavingAspect::class); - $container->registerAspect(TraitCompositionAspect::class); - $container->registerAspect(EnumMethodAspect::class); + $container->addLazyService(LoggingAspect::class, fn(): LoggingAspect => new LoggingAspect(new NullLogger())); + $container->addLazyService(DoSomethingAspect::class, fn(): DoSomethingAspect => new DoSomethingAspect()); + $container->addLazyService(ArrayPropertyInterceptAspect::class, fn(): ArrayPropertyInterceptAspect => new ArrayPropertyInterceptAspect()); + $container->addLazyService(PropertyInterceptAspect::class, fn(): PropertyInterceptAspect => new PropertyInterceptAspect()); + $container->addLazyService(PromotedPropertyInterceptAspect::class, fn(): PromotedPropertyInterceptAspect => new PromotedPropertyInterceptAspect()); + $container->addLazyService(Issue293Aspect::class, fn(): Issue293Aspect => new Issue293Aspect()); + $container->addLazyService(InitializationAspect::class, fn(): InitializationAspect => new InitializationAspect()); + $container->addLazyService(WeavingAspect::class, fn(): WeavingAspect => new WeavingAspect()); + $container->addLazyService(TraitCompositionAspect::class, fn(): TraitCompositionAspect => new TraitCompositionAspect()); + $container->addLazyService(EnumMethodAspect::class, fn(): EnumMethodAspect => new EnumMethodAspect()); } } diff --git a/tests/Fixtures/project/src/Kernel/InconsistentlyWeavingAspectKernel.php b/tests/Fixtures/project/src/Kernel/InconsistentlyWeavingAspectKernel.php index 9b79a375..4e6a63ae 100644 --- a/tests/Fixtures/project/src/Kernel/InconsistentlyWeavingAspectKernel.php +++ b/tests/Fixtures/project/src/Kernel/InconsistentlyWeavingAspectKernel.php @@ -21,11 +21,11 @@ class InconsistentlyWeavingAspectKernel extends AspectKernel */ protected function configureAop(AspectContainer $container): void { - $container->registerAspect(LoggingAspect::class, fn(): LoggingAspect => new LoggingAspect(new NullLogger())); + $container->addLazyService(LoggingAspect::class, fn(): LoggingAspect => new LoggingAspect(new NullLogger())); // Deliberately eager (instance) registration: the inconsistent-weaving scenario this // kernel exists to reproduce requires the application class to be loaded through the // AOP loader before weaving starts, which only happens when the aspect is constructed // during configureAop() rather than lazily on first use. - $container->registerAspect(new InconsistentlyWeavingAspect(new InconsistentlyWeavedClass())); + $container->add(InconsistentlyWeavingAspect::class, new InconsistentlyWeavingAspect(new InconsistentlyWeavedClass())); } }