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
10 changes: 5 additions & 5 deletions .github/workflows/phpstan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
- name: "Install PHP"
uses: shivammathur/setup-php@v2
with:
php-version: "8.2"
php-version: "8.4"
ini-values: memory_limit=-1
tools: composer:v2
- name: "Cache dependencies"
Expand All @@ -22,9 +22,9 @@ jobs:
path: |
~/.composer/cache
vendor
key: "php-8.2"
restore-keys: "php-8.2"
key: "php-8.4"
restore-keys: "php-8.4"
- name: "Install dependencies"
run: "composer install --no-interaction --no-progress --no-suggest"
run: "composer install --no-interaction --no-progress"
- name: "Static analysis"
uses: chindit/actions-phpstan@master
run: "vendor/bin/phpstan analyze --memory-limit=512M"
10 changes: 5 additions & 5 deletions .github/workflows/phpunit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ jobs:
- "lowest"
- "highest"
php-version:
- "8.2"
- "8.3"
- "8.4"
- "8.5"
operating-system:
- "ubuntu-latest"

Expand All @@ -44,15 +44,15 @@ jobs:

- name: "Install lowest dependencies"
if: ${{ matrix.dependencies == 'lowest' }}
run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest"
run: "composer update --prefer-lowest --no-interaction --no-progress"

- name: "Install highest dependencies"
if: ${{ matrix.dependencies == 'highest' }}
run: "composer update --no-interaction --no-progress --no-suggest"
run: "composer update --no-interaction --no-progress"

- name: "Install locked dependencies"
if: ${{ matrix.dependencies == 'locked' }}
run: "composer install --no-interaction --no-progress --no-suggest"
run: "composer install --no-interaction --no-progress"

- name: "Tests"
run: "vendor/bin/phpunit"
1 change: 1 addition & 0 deletions AGENTS.md
88 changes: 88 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

**Go! AOP Framework** — an Aspect-Oriented Programming (AOP) framework for PHP 8.4+. It intercepts PHP class/method/function execution transparently by transforming source code at load time via a custom PHP stream wrapper, without requiring PECL extensions, annotations at runtime, or eval.

Package: `goaop/framework` | Namespace root: `Go\` | PHP: `^8.4.0`

## Commands

```bash
# Install dependencies
composer install

# Run full test suite
./vendor/bin/phpunit

# Run a single test file
./vendor/bin/phpunit tests/Go/Core/ContainerTest.php

# Run a single test method
./vendor/bin/phpunit --filter testMethodName tests/Go/Core/ContainerTest.php

# Static analysis (level 4, src/ only)
./vendor/bin/phpstan analyze

# CLI debugging tools
./bin/aspect debug:advisors [class]
./bin/aspect debug:pointcuts [expression]
```

## Architecture

The framework works by intercepting PHP's class loading pipeline. When a class is loaded, the stream wrapper transforms its source code to inject interception hooks, then stores the result in a cache directory. The transformed class contains calls into the advisor chain for each matched join point.

### Initialization flow

1. **`AspectKernel::init()`** (`src/Core/AspectKernel.php`) — singleton, registers stream wrapper, builds transformer chain, calls `configureAop()` where users register aspects
2. **`SourceTransformingLoader::register()`** (`src/Instrument/ClassLoading/SourceTransformingLoader.php`) — PHP stream wrapper that intercepts `include`/`require` via the `go-aop-php://` protocol
3. **`AopComposerLoader::init()`** (`src/Instrument/ClassLoading/AopComposerLoader.php`) — hooks into Composer's autoloader to redirect loads through the stream wrapper
4. **`CachingTransformer`** — outer transformer that manages cache; on cache miss, invokes the inner transformers and writes the result

### Transformer chain (inner, registered in `AspectKernel::registerTransformers()`)

Applied in order for each loaded file:
- `ConstructorExecutionTransformer` — transforms `new` expressions (when `INTERCEPT_INITIALIZATIONS` feature enabled)
- `FilterInjectorTransformer` — wraps `include`/`require` (when `INTERCEPT_INCLUDES` enabled)
- `SelfValueTransformer` — rewrites `self::` to use the concrete proxy class
- `WeavingTransformer` — main transformer; uses `AdviceMatcher` to find applicable advices and `CachedAspectLoader` for aspect metadata, then delegates to proxy generators
- `MagicConstantTransformer` — rewrites `__FILE__`/`__DIR__` so they resolve to the original file, not the cached proxy

Each transformer returns `TransformerResultEnum`: `RESULT_TRANSFORMED`, `RESULT_ABSTAIN`, or `RESULT_ABORTED`.

### Proxy generation (`src/Proxy/`)

- `ClassProxyGenerator` — generates a proxy subclass with overridden interceptable methods
- `FunctionProxyGenerator` — generates function wrappers
- `TraitProxyGenerator` — generates trait proxies
- `src/Proxy/Part/` — individual code-generation components (method lists, parameter lists, joinpoint property injection)

### AOP core (`src/Aop/`)

- `src/Aop/Intercept/` — interfaces: `Joinpoint`, `Invocation`, `MethodInvocation`, `ConstructorInvocation`, `FunctionInvocation`, `FieldAccess`
- `src/Aop/Framework/` — concrete invocation implementations used at runtime by proxies; `AbstractMethodInvocation`, `DynamicClosureMethodInvocation`, `StaticClosureMethodInvocation`, `ClassFieldAccess`, etc.
- `src/Aop/Pointcut/` — LALR pointcut grammar (`PointcutGrammar`, `PointcutParser`, `PointcutLexer`, `PointcutParseTable`) and pointcut combinators (`AndPointcut`, `OrPointcut`, `NotPointcut`, `NamePointcut`, `AttributePointcut`, etc.)
- `src/Lang/Attribute/` — PHP 8 attributes for declaring aspects and advice: `#[Aspect]`, `#[Before]`, `#[After]`, `#[Around]`, `#[AfterThrowing]`, `#[Pointcut]`, `#[DeclareError]`, `#[DeclareParents]`
- `src/Aop/Features.php` — bitmask enum for optional features (`INTERCEPT_FUNCTIONS`, `INTERCEPT_INITIALIZATIONS`, `INTERCEPT_INCLUDES`)

### Container and aspect loading (`src/Core/`)

- `Container.php` — DI container with `add()` (by class-string or key), `getService()`, `addLazyService()` (Closure), and automatic tagging by interface
- `AspectLoader` / `CachedAspectLoader` — scan aspect classes for pointcut/advice attributes and produce `Advisor` instances
- `AttributeAspectLoaderExtension` — handles PHP 8 attribute-based aspect definitions
- `AdviceMatcher` — given a class reflector, returns the set of applicable advisors keyed by join point

### Bridge

`src/Bridge/Doctrine/MetadataLoadInterceptor.php` — workaround for Doctrine ORM entity weaving (Doctrine loads metadata before the kernel can intercept classes).

## Test conventions

- Tests mirror the `src/` structure under `tests/Go/`
- Functional/integration tests live in `tests/Go/Functional/`
- Test fixtures (stub classes for weaving) live in `tests/Go/Stubs/` and `tests/Fixtures/project/src/` (autoloaded as `Go\Tests\TestProject\`)
- PHPUnit 11, bootstrap is `vendor/autoload.php` (no separate test bootstrap)
- PHPStan baseline is `phpstan-baseline.php` — add new accepted errors there rather than inline suppression when appropriate
30 changes: 12 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Go! AOP is a modern aspect-oriented framework in plain PHP with rich features fo
[![Total Downloads](https://img.shields.io/packagist/dt/goaop/framework.svg)](https://packagist.org/packages/goaop/framework)
[![Daily Downloads](https://img.shields.io/packagist/dd/goaop/framework.svg)](https://packagist.org/packages/goaop/framework)
[![SensioLabs Insight](https://img.shields.io/sensiolabs/i/34549463-37d3-4368-94f5-812880d3ce4c.svg)](https://insight.sensiolabs.com/projects/34549463-37d3-4368-94f5-812880d3ce4c)
[![Minimum PHP Version](http://img.shields.io/badge/php-%3E%3D%208.2-8892BF.svg)](https://php.net/)
[![Minimum PHP Version](http://img.shields.io/badge/php-%3E%3D%208.4-8892BF.svg)](https://www.php.net/supported-versions.php)
[![License](https://img.shields.io/packagist/l/goaop/framework.svg)](https://packagist.org/packages/goaop/framework)

Features
Expand Down Expand Up @@ -66,7 +66,7 @@ After that just configure your web server to `demos/` folder and open it in your

Ask composer to download the latest version of Go! AOP framework with its dependencies by running the command:

``` bash
```bash
composer require goaop/framework
```

Expand All @@ -83,7 +83,7 @@ application in one place.
The framework provides base class to make it easier to create your own kernel.
To create your application kernel, extend the abstract class `Go\Core\AspectKernel`

``` php
```php
<?php
// app/ApplicationAspectKernel.php

Expand All @@ -98,12 +98,8 @@ class ApplicationAspectKernel extends AspectKernel

/**
* Configure an AspectContainer with advisors, aspects and pointcuts
*
* @param AspectContainer $container
*
* @return void
*/
protected function configureAop(AspectContainer $container)
protected function configureAop(AspectContainer $container): void
{
}
}
Expand Down Expand Up @@ -136,18 +132,18 @@ $applicationAspectKernel->init([
Aspect is the key element of AOP philosophy. Go! AOP framework just uses simple PHP classes for declaring aspects, which makes it possible to use all features of OOP for aspect classes.
As an example let's intercept all the methods and display their names:

``` php
```php
// Aspect/MonitorAspect.php

namespace Aspect;

use Go\Aop\Aspect;
use Go\Aop\Intercept\FieldAccess;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\After;
use Go\Lang\Annotation\Before;
use Go\Lang\Annotation\Around;
use Go\Lang\Annotation\Pointcut;
use Go\Lang\Attribute\After;
use Go\Lang\Attribute\Before;
use Go\Lang\Attribute\Around;
use Go\Lang\Attribute\Pointcut;

/**
* Monitor aspect
Expand All @@ -157,10 +153,8 @@ class MonitorAspect implements Aspect

/**
* Method that will be called before real method
*
* @param MethodInvocation $invocation Invocation
* @Before("execution(public Example->*(*))")
*/
#[Before("execution(public Example->*(*))")]
public function beforeMethodExecution(MethodInvocation $invocation)
{
echo 'Calling Before Interceptor for: ',
Expand All @@ -173,8 +167,8 @@ class MonitorAspect implements Aspect
```

Easy, isn't it? We declared here that we want to install a hook before the execution of
all dynamic public methods in the class Example. This is done with the help of annotation
`@Before("execution(public Example->*(*))")`
all dynamic public methods in the class Example. This is done with the help of attribute
`#[Before("execution(public Example->*(*))")]`
Hooks can be of any types, you will see them later.
But we don't change any code in the class Example! I can feel your astonishment now.

Expand Down
10 changes: 5 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@
"license": "MIT",

"require": {
"php": "^8.2.0",
"php": "^8.4.0",
"ext-tokenizer": "*",
"goaop/parser-reflection": "4.x-dev",
"goaop/dissect": "^3.0",
"laminas/laminas-code": "^4.13",
"symfony/finder": "^5.4 || ^6.4 || ^7.0"
"laminas/laminas-code": "^4.17",
"symfony/finder": "^6.4 || ^7.0"
},

"require-dev": {
"adlawson/vfs": "^0.12.1",
"doctrine/orm": "^2.5 || ^3.0",
"phpstan/phpstan": "^1.10.57",
"phpunit/phpunit": "^10.5.10",
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^11.0",
"symfony/console": "^6.4 || ^7.0",
"symfony/filesystem": "^6.4 || ^7.0",
"symfony/process": "^6.4 || ^7.0",
Expand Down
8 changes: 5 additions & 3 deletions phpstan-baseline.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

$ignoreErrors = [];
$ignoreErrors[] = [
'message' => '#^Property Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadataInfo\\<object\\>\\:\\:\\$table \\(array\\{name\\: string, schema\\?\\: string, indexes\\?\\: array, uniqueConstraints\\?\\: array, options\\?\\: array\\<string, mixed\\>, quoted\\?\\: bool\\}\\) does not accept array\\{\\}\\.$#',
'message' => '#^Property Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata\\<object\\>\\:\\:\\$table \\(array\\{name\\: string, schema\\?\\: string, indexes\\?\\: array, uniqueConstraints\\?\\: array, options\\?\\: array\\<string, mixed\\>, quoted\\?\\: bool\\}\\) does not accept array\\{\\}\\.$#',
'identifier' => 'assign.propertyType',
'count' => 1,
'path' => __DIR__ . '/src/Bridge/Doctrine/MetadataLoadInterceptor.php',
];
$ignoreErrors[] = [
'message' => '#^Call to function file_get_contents\\(\\) on a separate line has no effect\\.$#',
'message' => '#^Trait Go\\\\Proxy\\\\Part\\\\PropertyInterceptionTrait is used zero times and is not analysed\\.$#',
'identifier' => 'trait.unused',
'count' => 1,
'path' => __DIR__ . '/src/Instrument/ClassLoading/CacheWarmer.php',
'path' => __DIR__ . '/src/Proxy/Part/PropertyInterceptionTrait.php',
];

return ['parameters' => ['ignoreErrors' => $ignoreErrors]];
4 changes: 2 additions & 2 deletions src/Aop/Pointcut.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public function getKind(): int;
public function matches(
ReflectionClass|ReflectionFileNamespace $context,
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null,
object|string $instanceOrScope = null,
array $arguments = null
null|object|string $instanceOrScope = null,
?array $arguments = null
): bool;
}
6 changes: 3 additions & 3 deletions src/Aop/Pointcut/AndPointcut.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
/**
* And constructor
*/
public function __construct(int $pointcutKind = null, Pointcut ...$pointcuts)
public function __construct(?int $pointcutKind = null, Pointcut ...$pointcuts)
{
// If we don't have specified kind, it will be calculated as intersection then
if (!isset($pointcutKind)) {
Expand All @@ -55,8 +55,8 @@ public function __construct(int $pointcutKind = null, Pointcut ...$pointcuts)
public function matches(
ReflectionClass|ReflectionFileNamespace $context,
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null,
object|string $instanceOrScope = null,
array $arguments = null
null|object|string $instanceOrScope = null,
?array $arguments = null
): bool {
foreach ($this->pointcuts as $singlePointcut) {
if (!$singlePointcut->matches($context, $reflector, $instanceOrScope, $arguments)) {
Expand Down
4 changes: 2 additions & 2 deletions src/Aop/Pointcut/AttributePointcut.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ public function __construct(
final public function matches(
ReflectionClass|ReflectionFileNamespace $context,
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null,
object|string $instanceOrScope = null,
array $arguments = null
null|object|string $instanceOrScope = null,
?array $arguments = null
): bool {
// If we don't use context for matching and we do static check, then always match
if (!$this->useContextForMatching && !isset($reflector)) {
Expand Down
4 changes: 2 additions & 2 deletions src/Aop/Pointcut/ClassInheritancePointcut.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ public function __construct(private string $parentClassOrInterfaceName) {}
public function matches(
ReflectionClass|ReflectionFileNamespace $context,
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null,
object|string $instanceOrScope = null,
array $arguments = null
null|object|string $instanceOrScope = null,
?array $arguments = null
): bool {
// We match only with ReflectionClass as a context
if (!$context instanceof ReflectionClass) {
Expand Down
4 changes: 2 additions & 2 deletions src/Aop/Pointcut/MagicMethodDynamicPointcut.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ public function __construct(private string $methodName) {
public function matches(
ReflectionClass|ReflectionFileNamespace $context,
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null,
object|string $instanceOrScope = null,
array $arguments = null
null|object|string $instanceOrScope = null,
?array $arguments = null
): bool {
// Magic methods can be only inside class context
if (!$context instanceof ReflectionClass) {
Expand Down
4 changes: 2 additions & 2 deletions src/Aop/Pointcut/MatchInheritedPointcut.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ final class MatchInheritedPointcut implements Pointcut
public function matches(
ReflectionClass|ReflectionFileNamespace $context,
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null,
object|string $instanceOrScope = null,
array $arguments = null
null|object|string $instanceOrScope = null,
?array $arguments = null
): bool {
// Inherited items can be only inside class context
if (!$context instanceof ReflectionClass) {
Expand Down
4 changes: 2 additions & 2 deletions src/Aop/Pointcut/ModifierPointcut.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ public function __construct(int $initialMask = 0)
public function matches(
ReflectionClass|ReflectionFileNamespace $context,
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null,
object|string $instanceOrScope = null,
array $arguments = null
null|object|string $instanceOrScope = null,
?array $arguments = null
): bool {
// With context only we always match, as we don't know about modifiers of given reflector
if (!isset($reflector)) {
Expand Down
4 changes: 2 additions & 2 deletions src/Aop/Pointcut/NamePointcut.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ public function __construct(
public function matches(
ReflectionClass|ReflectionFileNamespace $context,
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null,
object|string $instanceOrScope = null,
array $arguments = null
null|object|string $instanceOrScope = null,
?array $arguments = null
): bool {
// Let's determine what will be used for matching - context or reflector
if ($this->useContextForMatching) {
Expand Down
4 changes: 2 additions & 2 deletions src/Aop/Pointcut/NotPointcut.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ public function __construct(private Pointcut $pointcut) {}
public function matches(
ReflectionClass|ReflectionFileNamespace $context,
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null,
object|string $instanceOrScope = null,
array $arguments = null
null|object|string $instanceOrScope = null,
?array $arguments = null
): bool {
// For Logical "not" expression without reflector, we should match statically for any context
if (!isset($reflector)) {
Expand Down
Loading
Loading