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 @@ -6,7 +6,7 @@ See [UPGRADE-4.0.md](UPGRADE-4.0.md) for the migration guide.

**Requirements**
* [BC BREAK] Requires PHP 8.4+
* [BC BREAK] **Dependencies** — `ext-tokenizer`, `goaop/parser-reflection` ^4.0.1 || ^5.0.1, `goaop/dissect` ^4.0 and `symfony/finder` ^7.4 || ^8.0. `nikic/php-parser` 5 comes through parser-reflection and is no longer required directly. `symfony/console` ^7.4 || ^8.0 is needed for the `bin/aspect` tool.
* [BC BREAK] **Dependencies** — `ext-tokenizer`, `goaop/parser-reflection` ^4.0.1 || ^5.0.1, `goaop/dissect` ^4.0, `psr/container` ^1.1 || ^2.0 and `symfony/finder` ^7.4 || ^8.0. `nikic/php-parser` 5 comes through parser-reflection and is no longer required directly. `symfony/console` ^7.4 || ^8.0 is needed for the `bin/aspect` tool.

**Added**
* [Feature] **First-class callable advices** — the main way advices are now wired into woven code. Generated proxies declare each aspect-method advice as `Interceptor::before(The::aspect(MonitorAspect::class)->beforeMethodExecution(...))` — an eager first-class callable, since the interceptor list is only built while the intercepted method or hook is already executing (`LazyAdvisorAccessor` is removed). Compiled advisor cache files use the same facade in its lazy static-data form, e.g. `Interceptor::before(MonitorAspect::class, 'beforeMethodExecution')`, which returns a native PHP lazy proxy: interceptor construction, aspect resolution from the container and advice callable creation are all deferred until the advice is actually used, so cached advices whose pointcut never matches never instantiate their aspect. Advices registered in the container as plain closures (not aspect methods) are resolved through the new `The::advice('advisorId')` accessor, which unwraps `Advisor` and interceptor values down to the raw advice closure.
Expand All @@ -18,6 +18,7 @@ See [UPGRADE-4.0.md](UPGRADE-4.0.md) for the migration guide.
* [Feature] **Generic types for static analysis** — the core joinpoint interfaces (`Joinpoint`, `Invocation`, `MethodInvocation`, `ConstructorInvocation`, `FieldAccess`, `FunctionInvocation`, ...) declare `@template` types for the target class and the return value, so PHPStan and IDEs know the types of `getThis()`, `proceed()` and friends inside advices.
* [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] 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
25 changes: 23 additions & 2 deletions UPGRADE-4.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ Requirements
------------

- PHP 8.4+ with `ext-tokenizer`.
- `goaop/parser-reflection` ^4.0.1 || ^5.0.1, `goaop/dissect` ^4.0, `symfony/finder` ^7.4 || ^8.0.
- `goaop/parser-reflection` ^4.0.1 || ^5.0.1, `goaop/dissect` ^4.0, `symfony/finder` ^7.4 || ^8.0,
`psr/container` ^1.1 || ^2.0.
`nikic/php-parser` 5 is installed through parser-reflection.
- `laminas/laminas-code` is no longer used: proxies are generated by the internal
`Go\Proxy\Generator` package. Code that extended the framework generators or relied on laminas
Expand Down Expand Up @@ -113,6 +114,24 @@ object of the wrong type now fails on first use, not at registration.
`AspectContainer::addResource()` is part of the interface. `onRegistration()` lets you react to
deferred registrations of services implementing an interface.

### PSR-11 container

`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()`.

```php
// before
$pointcut = $container->getValue('my.pointcut');

// 4.0
$pointcut = $container->get('my.pointcut');
```

An unknown id throws `ServiceNotFoundException`, which implements `Psr\Container\NotFoundExceptionInterface`.
`getService()` throws `Go\Aop\Exception\ContainerException` (an `InvalidConfigurationException` implementing
`Psr\Container\ContainerExceptionInterface`) when the entry is not an instance of the requested class.

### Advice and interceptor API

- The `AdviceBefore`, `AdviceAfter` and `AdviceAround` marker interfaces are removed. `Advice`
Expand Down Expand Up @@ -206,6 +225,7 @@ Renamed enums and constants
| `TransformerResultEnum::RESULT_ABORTED` | `TransformerResult::Aborted` |
| `FieldAccessType::READ` / `WRITE` | `FieldAccessType::Read` / `Write` |
| `AspectContainer::AOP_PROXIED_SUFFIX` | `AspectContainer::ORIGINAL_TRAIT_SUFFIX` |
| `AspectContainer::getValue()` | `AspectContainer::get()` (PSR-11) |

The `Go\Proxy\Generator` enums (`Visibility`, `PropertyModifier`, `ClassModifier`) use PascalCase cases as well.
Custom source transformers return `TransformerResult` cases, and advices comparing `getAccessType()` use the new
Expand Down Expand Up @@ -258,7 +278,8 @@ any Go! AOP failure. Several throw sites changed their type:
|------------------------------------------------------------------|--------------------------------------------------------|-----|
| Invalid kernel options, container registrations, console input | `RuntimeException`, `InvalidArgumentException`, `UnexpectedValueException` | `InvalidConfigurationException` (an `InvalidArgumentException`) |
| Invalid pointcut expression | `UnexpectedValueException`, `InvalidArgumentException` | `PointcutSyntaxException` (an `AspectException`) |
| Unknown container id | `OutOfBoundsException` | `ServiceNotFoundException` (an `OutOfBoundsException`) |
| Unknown container id | `OutOfBoundsException` | `ServiceNotFoundException` (an `OutOfBoundsException` and a PSR-11 `NotFoundExceptionInterface`) |
| Container entry of the wrong type in `getService()` | `InvalidArgumentException` | `ContainerException` (an `InvalidConfigurationException` and a PSR-11 `ContainerExceptionInterface`) |
| Weaving, proxy generation, cache writes | `RuntimeException`, `LogicException`, `InvalidArgumentException` | `WeavingException` (a `RuntimeException`) |
| Advisor that can not be cached | `Go\Core\Cache\NotCompilableException` | `Go\Aop\Exception\NotCompilableException` |

Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"ext-tokenizer": "*",
"goaop/dissect": "^4.0",
"goaop/parser-reflection": "^4.0.1 || ^5.0.1",
"psr/container": "^1.1 || ^2.0",
"symfony/finder": "^7.4 || ^8.0"
},
"require-dev": {
Expand Down
4 changes: 2 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ bit throws `Go\Aop\Exception\InvalidConfigurationException`.
| `appDir` | `string` | the directory that contains `vendor/` | Application root. Cache paths mirror source paths relative to it, and only files below it are woven. |
| `debug` | `bool` | `false` | Development mode: changes of the kernel file and of aspect files invalidate the cache, and every class and `include` is loaded through the weaving stream filter. With `false`, woven classes are loaded from the cache through Composer's class map. Use `false` in production. |
| `includePaths` | `string[]` | `[]` (everything below `appDir`) | Directories that are woven. Keep it narrow: weaving vendor code is slow and rarely needed. |
| `excludePaths` | `string[]` | `[]` | Directories that are never woven. The cache directory, the framework itself and its runtime dependencies (goaop/dissect, goaop/parser-reflection, nikic/php-parser, symfony/finder) are always excluded. |
| `excludePaths` | `string[]` | `[]` | Directories that are never woven. The cache directory, the framework itself and its runtime dependencies (goaop/dissect, goaop/parser-reflection, nikic/php-parser, psr/container, symfony/finder) are always excluded. |
| `features` | `int` | `0` | Bitmask of `Go\Aop\Features` flags, see below. |
| `cacheFileMode` | `int` | `0770` minus the umask | Permissions of cache files. Must be between `0600` and `0777` and give the owner read and write access; directories get matching search bits. |
| `containerClass` | `class-string` | `Go\Core\Container` | Aspect container implementation, must implement `Go\Core\AspectContainer`. |
| `containerClass` | `class-string` | `Go\Core\Container` | Aspect container implementation, must implement `Go\Core\AspectContainer` (a PSR-11 `ContainerInterface`). |

## Features

Expand Down
20 changes: 20 additions & 0 deletions src/Aop/Exception/ContainerException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);
/*
* Go! AOP framework
*
* @copyright Copyright 2026, Lisachenko Alexander <lisachenko.it@gmail.com>
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/

namespace Go\Aop\Exception;

use Psr\Container\ContainerExceptionInterface;

/**
* A container entry that is registered, but can not be retrieved as declared
*/
final class ContainerException extends InvalidConfigurationException implements ContainerExceptionInterface {}
3 changes: 2 additions & 1 deletion src/Aop/Exception/ServiceNotFoundException.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
namespace Go\Aop\Exception;

use OutOfBoundsException;
use Psr\Container\NotFoundExceptionInterface;

/**
* A service or value that is not registered in the aspect container
*/
class ServiceNotFoundException extends OutOfBoundsException implements ExceptionInterface {}
class ServiceNotFoundException extends OutOfBoundsException implements ExceptionInterface, NotFoundExceptionInterface {}
2 changes: 1 addition & 1 deletion src/Aop/Framework/The.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public static function aspect(string $aspectClass): Aspect

public static function advice(string $advisorId): Closure
{
$value = self::getContainer()->getValue($advisorId);
$value = self::getContainer()->get($advisorId);

if ($value instanceof Advisor) {
$value = $value->getAdvice();
Expand Down
2 changes: 1 addition & 1 deletion src/Aop/Pointcut/PointcutReference.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public function compileToPhp(): Expr
private function getPointcut(): Pointcut
{
if (!isset($this->pointcut)) {
$pointcutValue = $this->container->getValue($this->pointcutId);
$pointcutValue = $this->container->get($this->pointcutId);
if (!$pointcutValue instanceof Pointcut) {
throw new AspectException("Pointcut reference {$this->pointcutId} resolves to " . get_debug_type($pointcutValue) . ', not to a pointcut');
}
Expand Down
4 changes: 2 additions & 2 deletions src/Core/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# src/Core — Container and aspect loading

## Container (Container.php)
- Generic, AOP-unaware DI container: add(by class-string|key), getService(), getValue(), 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), onRegistration(interfaceFQCN, listener), addResource()
- 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
- Aspects are typical services — no registerAspect(); the kernel arms a debug-only Aspect::class onRegistration listener for resource tracking (files resolved via AopComposerLoader::findOriginalFile(), aspects stay unloaded) and registers framework services via FrameworkServices::register(); CachePathManager is added eagerly by the kernel (the class loader reads its cache index right away)
- Throws SPL exceptions only (InvalidArgumentException, UnexpectedValueException, OutOfBoundsException)
- Throws framework exceptions: ServiceNotFoundException (unknown id, PSR NotFoundExceptionInterface), ContainerException (entry of the wrong type, PSR ContainerExceptionInterface), InvalidConfigurationException (invalid registration)

## Aspect loading
- AspectLoader — scans aspect classes for pointcut/advice attributes → Advisor[]; CachedAspectLoader decorates it (both implement AspectLoaderInterface)
Expand Down
24 changes: 11 additions & 13 deletions src/Core/AspectContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
namespace Go\Core;

use Closure;
use OutOfBoundsException;
use Go\Aop\Exception\ContainerException;
use Go\Aop\Exception\ServiceNotFoundException;
use Psr\Container\ContainerInterface;

/**
* Aspect container interface
* Aspect container interface, a PSR-11 container with the registration API of the framework
*/
interface AspectContainer
interface AspectContainer extends ContainerInterface
{
/**
* Prefix for function interceptor
Expand Down Expand Up @@ -81,7 +83,8 @@ interface AspectContainer
*
* @template T of object
*
* @throws OutOfBoundsException if service was not found
* @throws ServiceNotFoundException if service was not found
* @throws ContainerException if the entry is not an instance of the given class
*/
public function getService(string $className): object;

Expand All @@ -98,19 +101,14 @@ public function getServicesByInterface(string $interfaceTagClassName): array;
/**
* Returns a value from the container
*
* @param string $key Given key
* @param string $id Identifier of the entry
*
* @return ($key is class-string<T> ? T : mixed)
* @throws OutOfBoundsException if key was not found
* @return ($id is class-string<T> ? T : mixed)
* @throws ServiceNotFoundException if no entry was found for the identifier
*
* @template T of object
*/
public function getValue(string $key): mixed;

/**
* Checks if item with specified id is present in the container
*/
public function has(string $id): bool;
public function get(string $id): mixed;

/**
* Registers a listener that is called whenever a deferred service whose id may be a
Expand Down
1 change: 1 addition & 0 deletions src/Core/AspectKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ abstract class AspectKernel
'goaop/dissect',
'goaop/parser-reflection',
'nikic/php-parser',
'psr/container',
'symfony/finder',
];

Expand Down
23 changes: 12 additions & 11 deletions src/Core/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
namespace Go\Core;

use Closure;
use Go\Aop\Exception\ContainerException;
use Go\Aop\Exception\InvalidConfigurationException;
use Go\Aop\Exception\ServiceNotFoundException;
use ReflectionClass;

/**
* DI-container
* DI-container, PSR-11 compatible
*/
final class Container implements AspectContainer
{
Expand Down Expand Up @@ -132,26 +133,26 @@ public function addLazyService(string $id, Closure $lazyInitializationClosure):

public function getService(string $className): object
{
$service = $this->getValue($className);
$service = $this->get($className);
if (!$service instanceof $className) {
throw new InvalidConfigurationException("Service $className is not properly registered");
throw new ContainerException("Service $className is not properly registered");
}

return $service;
}

public function getValue(string $key): mixed
public function get(string $id): mixed
{
// isset() first for speed, array_key_exists() keeps a stored null value retrievable
if (!isset($this->values[$key]) && !array_key_exists($key, $this->values)) {
if (isset($this->factories[$key])) {
$this->materializeService($key);
if (!isset($this->values[$id]) && !array_key_exists($id, $this->values)) {
if (isset($this->factories[$id])) {
$this->materializeService($id);
} else {
throw new ServiceNotFoundException("Value $key is not defined in the container");
throw new ServiceNotFoundException("Value $id is not defined in the container");
}
}

return $this->values[$key];
return $this->values[$id];
}

public function has(string $id): bool
Expand Down Expand Up @@ -191,7 +192,7 @@ public function getServicesByInterface(string $interfaceTagClassName): array
$version = $this->registrationVersion;
$values = [];
foreach (($this->tags[$interfaceTagClassName] ?? []) as $containerKey) {
$values[$containerKey] = $this->getValue($containerKey);
$values[$containerKey] = $this->get($containerKey);
}
// A re-entrant registration while collecting the values invalidates this result
if ($isConclusive && $version === $this->registrationVersion) {
Expand Down Expand Up @@ -239,7 +240,7 @@ private function createLazyService(string $id, Closure $factory): object
return NativeLazyProxy::tryCreate($id, function () use ($id, $factory): object {
$instance = $factory($this);
if (!$instance instanceof $id) {
throw new InvalidConfigurationException("Service $id is not properly registered");
throw new ContainerException("Service $id is not properly registered");
}

return $instance;
Expand Down
Loading
Loading