Skip to content
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

//...
Expand Down
4 changes: 3 additions & 1 deletion UPGRADE-4.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/Core/AGENTS.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
32 changes: 31 additions & 1 deletion src/Core/AspectContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -104,7 +105,7 @@ public function getServicesByInterface(string $interfaceTagClassName): array;
* @param string $id Identifier of the entry
*
* @return ($id is class-string<T> ? 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
*/
Expand Down Expand Up @@ -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<T> $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
*
Expand Down
68 changes: 67 additions & 1 deletion src/Core/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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<class-string, Closure(AspectContainer): object> Deferred service factories, keyed by class-name
*/
Expand Down Expand Up @@ -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");
}
Expand All @@ -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
Expand Down
Loading
Loading