Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<?php
Expand All @@ -316,7 +318,10 @@ use Aspect\MonitorAspect;

protected function configureAop(AspectContainer $container)
{
$container->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());
}

//...
Expand Down
16 changes: 8 additions & 8 deletions demos/Demo/Aspect/AwesomeAspectKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
2 changes: 1 addition & 1 deletion src/Aop/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Aspect>|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('<id>') stays eager too. Free to change between releases
- Interceptor — @internal factory facade with TWO construction modes: before()/after()/around()/afterThrowing(class-string<Aspect>|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('<id>') 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)
Expand Down
7 changes: 5 additions & 2 deletions src/Aop/Framework/Interceptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions src/Core/AGENTS.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
36 changes: 22 additions & 14 deletions src/Core/AspectContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

use Closure;
use OutOfBoundsException;
use Go\Aop\Aspect;

/**
* Aspect container interface
Expand Down Expand Up @@ -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<Aspect> $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
Expand Down Expand Up @@ -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;
}
21 changes: 21 additions & 0 deletions src/Core/AspectKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

namespace Go\Core;

use Go\Aop\Aspect;
use Go\Aop\AspectException;
use Go\Aop\Features;
use Go\Core\Cache\CachedAspectLoader;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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);

Expand Down
Loading
Loading