Skip to content

Commit 293a54e

Browse files
authored
Merge pull request #516 from goaop/feature/ai-tools-initialization
Claude: init AI context and update minimum php version to php>=8.4
2 parents 66d5028 + 597c042 commit 293a54e

30 files changed

Lines changed: 206 additions & 165 deletions

.github/workflows/phpstan.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
- name: "Install PHP"
1414
uses: shivammathur/setup-php@v2
1515
with:
16-
php-version: "8.2"
16+
php-version: "8.4"
1717
ini-values: memory_limit=-1
1818
tools: composer:v2
1919
- name: "Cache dependencies"
@@ -22,9 +22,9 @@ jobs:
2222
path: |
2323
~/.composer/cache
2424
vendor
25-
key: "php-8.2"
26-
restore-keys: "php-8.2"
25+
key: "php-8.4"
26+
restore-keys: "php-8.4"
2727
- name: "Install dependencies"
28-
run: "composer install --no-interaction --no-progress --no-suggest"
28+
run: "composer install --no-interaction --no-progress"
2929
- name: "Static analysis"
30-
uses: chindit/actions-phpstan@master
30+
run: "vendor/bin/phpstan analyze --memory-limit=512M"

.github/workflows/phpunit.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ jobs:
1616
- "lowest"
1717
- "highest"
1818
php-version:
19-
- "8.2"
20-
- "8.3"
19+
- "8.4"
20+
- "8.5"
2121
operating-system:
2222
- "ubuntu-latest"
2323

@@ -44,15 +44,15 @@ jobs:
4444

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

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

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

5757
- name: "Tests"
5858
run: "vendor/bin/phpunit"

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CLAUDE.md

CLAUDE.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
**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.
8+
9+
Package: `goaop/framework` | Namespace root: `Go\` | PHP: `^8.4.0`
10+
11+
## Commands
12+
13+
```bash
14+
# Install dependencies
15+
composer install
16+
17+
# Run full test suite
18+
./vendor/bin/phpunit
19+
20+
# Run a single test file
21+
./vendor/bin/phpunit tests/Go/Core/ContainerTest.php
22+
23+
# Run a single test method
24+
./vendor/bin/phpunit --filter testMethodName tests/Go/Core/ContainerTest.php
25+
26+
# Static analysis (level 4, src/ only)
27+
./vendor/bin/phpstan analyze
28+
29+
# CLI debugging tools
30+
./bin/aspect debug:advisors [class]
31+
./bin/aspect debug:pointcuts [expression]
32+
```
33+
34+
## Architecture
35+
36+
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.
37+
38+
### Initialization flow
39+
40+
1. **`AspectKernel::init()`** (`src/Core/AspectKernel.php`) — singleton, registers stream wrapper, builds transformer chain, calls `configureAop()` where users register aspects
41+
2. **`SourceTransformingLoader::register()`** (`src/Instrument/ClassLoading/SourceTransformingLoader.php`) — PHP stream wrapper that intercepts `include`/`require` via the `go-aop-php://` protocol
42+
3. **`AopComposerLoader::init()`** (`src/Instrument/ClassLoading/AopComposerLoader.php`) — hooks into Composer's autoloader to redirect loads through the stream wrapper
43+
4. **`CachingTransformer`** — outer transformer that manages cache; on cache miss, invokes the inner transformers and writes the result
44+
45+
### Transformer chain (inner, registered in `AspectKernel::registerTransformers()`)
46+
47+
Applied in order for each loaded file:
48+
- `ConstructorExecutionTransformer` — transforms `new` expressions (when `INTERCEPT_INITIALIZATIONS` feature enabled)
49+
- `FilterInjectorTransformer` — wraps `include`/`require` (when `INTERCEPT_INCLUDES` enabled)
50+
- `SelfValueTransformer` — rewrites `self::` to use the concrete proxy class
51+
- `WeavingTransformer` — main transformer; uses `AdviceMatcher` to find applicable advices and `CachedAspectLoader` for aspect metadata, then delegates to proxy generators
52+
- `MagicConstantTransformer` — rewrites `__FILE__`/`__DIR__` so they resolve to the original file, not the cached proxy
53+
54+
Each transformer returns `TransformerResultEnum`: `RESULT_TRANSFORMED`, `RESULT_ABSTAIN`, or `RESULT_ABORTED`.
55+
56+
### Proxy generation (`src/Proxy/`)
57+
58+
- `ClassProxyGenerator` — generates a proxy subclass with overridden interceptable methods
59+
- `FunctionProxyGenerator` — generates function wrappers
60+
- `TraitProxyGenerator` — generates trait proxies
61+
- `src/Proxy/Part/` — individual code-generation components (method lists, parameter lists, joinpoint property injection)
62+
63+
### AOP core (`src/Aop/`)
64+
65+
- `src/Aop/Intercept/` — interfaces: `Joinpoint`, `Invocation`, `MethodInvocation`, `ConstructorInvocation`, `FunctionInvocation`, `FieldAccess`
66+
- `src/Aop/Framework/` — concrete invocation implementations used at runtime by proxies; `AbstractMethodInvocation`, `DynamicClosureMethodInvocation`, `StaticClosureMethodInvocation`, `ClassFieldAccess`, etc.
67+
- `src/Aop/Pointcut/` — LALR pointcut grammar (`PointcutGrammar`, `PointcutParser`, `PointcutLexer`, `PointcutParseTable`) and pointcut combinators (`AndPointcut`, `OrPointcut`, `NotPointcut`, `NamePointcut`, `AttributePointcut`, etc.)
68+
- `src/Lang/Attribute/` — PHP 8 attributes for declaring aspects and advice: `#[Aspect]`, `#[Before]`, `#[After]`, `#[Around]`, `#[AfterThrowing]`, `#[Pointcut]`, `#[DeclareError]`, `#[DeclareParents]`
69+
- `src/Aop/Features.php` — bitmask enum for optional features (`INTERCEPT_FUNCTIONS`, `INTERCEPT_INITIALIZATIONS`, `INTERCEPT_INCLUDES`)
70+
71+
### Container and aspect loading (`src/Core/`)
72+
73+
- `Container.php` — DI container with `add()` (by class-string or key), `getService()`, `addLazyService()` (Closure), and automatic tagging by interface
74+
- `AspectLoader` / `CachedAspectLoader` — scan aspect classes for pointcut/advice attributes and produce `Advisor` instances
75+
- `AttributeAspectLoaderExtension` — handles PHP 8 attribute-based aspect definitions
76+
- `AdviceMatcher` — given a class reflector, returns the set of applicable advisors keyed by join point
77+
78+
### Bridge
79+
80+
`src/Bridge/Doctrine/MetadataLoadInterceptor.php` — workaround for Doctrine ORM entity weaving (Doctrine loads metadata before the kernel can intercept classes).
81+
82+
## Test conventions
83+
84+
- Tests mirror the `src/` structure under `tests/Go/`
85+
- Functional/integration tests live in `tests/Go/Functional/`
86+
- Test fixtures (stub classes for weaving) live in `tests/Go/Stubs/` and `tests/Fixtures/project/src/` (autoloaded as `Go\Tests\TestProject\`)
87+
- PHPUnit 11, bootstrap is `vendor/autoload.php` (no separate test bootstrap)
88+
- PHPStan baseline is `phpstan-baseline.php` — add new accepted errors there rather than inline suppression when appropriate

README.md

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Go! AOP is a modern aspect-oriented framework in plain PHP with rich features fo
88
[![Total Downloads](https://img.shields.io/packagist/dt/goaop/framework.svg)](https://packagist.org/packages/goaop/framework)
99
[![Daily Downloads](https://img.shields.io/packagist/dd/goaop/framework.svg)](https://packagist.org/packages/goaop/framework)
1010
[![SensioLabs Insight](https://img.shields.io/sensiolabs/i/34549463-37d3-4368-94f5-812880d3ce4c.svg)](https://insight.sensiolabs.com/projects/34549463-37d3-4368-94f5-812880d3ce4c)
11-
[![Minimum PHP Version](http://img.shields.io/badge/php-%3E%3D%208.2-8892BF.svg)](https://php.net/)
11+
[![Minimum PHP Version](http://img.shields.io/badge/php-%3E%3D%208.4-8892BF.svg)](https://www.php.net/supported-versions.php)
1212
[![License](https://img.shields.io/packagist/l/goaop/framework.svg)](https://packagist.org/packages/goaop/framework)
1313

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

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

69-
``` bash
69+
```bash
7070
composer require goaop/framework
7171
```
7272

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

86-
``` php
86+
```php
8787
<?php
8888
// app/ApplicationAspectKernel.php
8989

@@ -98,12 +98,8 @@ class ApplicationAspectKernel extends AspectKernel
9898

9999
/**
100100
* Configure an AspectContainer with advisors, aspects and pointcuts
101-
*
102-
* @param AspectContainer $container
103-
*
104-
* @return void
105101
*/
106-
protected function configureAop(AspectContainer $container)
102+
protected function configureAop(AspectContainer $container): void
107103
{
108104
}
109105
}
@@ -136,18 +132,18 @@ $applicationAspectKernel->init([
136132
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.
137133
As an example let's intercept all the methods and display their names:
138134

139-
``` php
135+
```php
140136
// Aspect/MonitorAspect.php
141137

142138
namespace Aspect;
143139

144140
use Go\Aop\Aspect;
145141
use Go\Aop\Intercept\FieldAccess;
146142
use Go\Aop\Intercept\MethodInvocation;
147-
use Go\Lang\Annotation\After;
148-
use Go\Lang\Annotation\Before;
149-
use Go\Lang\Annotation\Around;
150-
use Go\Lang\Annotation\Pointcut;
143+
use Go\Lang\Attribute\After;
144+
use Go\Lang\Attribute\Before;
145+
use Go\Lang\Attribute\Around;
146+
use Go\Lang\Attribute\Pointcut;
151147

152148
/**
153149
* Monitor aspect
@@ -157,10 +153,8 @@ class MonitorAspect implements Aspect
157153

158154
/**
159155
* Method that will be called before real method
160-
*
161-
* @param MethodInvocation $invocation Invocation
162-
* @Before("execution(public Example->*(*))")
163156
*/
157+
#[Before("execution(public Example->*(*))")]
164158
public function beforeMethodExecution(MethodInvocation $invocation)
165159
{
166160
echo 'Calling Before Interceptor for: ',
@@ -173,8 +167,8 @@ class MonitorAspect implements Aspect
173167
```
174168

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

composer.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,19 @@
88
"license": "MIT",
99

1010
"require": {
11-
"php": "^8.2.0",
11+
"php": "^8.4.0",
1212
"ext-tokenizer": "*",
1313
"goaop/parser-reflection": "4.x-dev",
1414
"goaop/dissect": "^3.0",
15-
"laminas/laminas-code": "^4.13",
16-
"symfony/finder": "^5.4 || ^6.4 || ^7.0"
15+
"laminas/laminas-code": "^4.17",
16+
"symfony/finder": "^6.4 || ^7.0"
1717
},
1818

1919
"require-dev": {
2020
"adlawson/vfs": "^0.12.1",
2121
"doctrine/orm": "^2.5 || ^3.0",
22-
"phpstan/phpstan": "^1.10.57",
23-
"phpunit/phpunit": "^10.5.10",
22+
"phpstan/phpstan": "^2.0",
23+
"phpunit/phpunit": "^11.0",
2424
"symfony/console": "^6.4 || ^7.0",
2525
"symfony/filesystem": "^6.4 || ^7.0",
2626
"symfony/process": "^6.4 || ^7.0",

phpstan-baseline.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22

33
$ignoreErrors = [];
44
$ignoreErrors[] = [
5-
'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\\{\\}\\.$#',
5+
'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\\{\\}\\.$#',
6+
'identifier' => 'assign.propertyType',
67
'count' => 1,
78
'path' => __DIR__ . '/src/Bridge/Doctrine/MetadataLoadInterceptor.php',
89
];
910
$ignoreErrors[] = [
10-
'message' => '#^Call to function file_get_contents\\(\\) on a separate line has no effect\\.$#',
11+
'message' => '#^Trait Go\\\\Proxy\\\\Part\\\\PropertyInterceptionTrait is used zero times and is not analysed\\.$#',
12+
'identifier' => 'trait.unused',
1113
'count' => 1,
12-
'path' => __DIR__ . '/src/Instrument/ClassLoading/CacheWarmer.php',
14+
'path' => __DIR__ . '/src/Proxy/Part/PropertyInterceptionTrait.php',
1315
];
1416

1517
return ['parameters' => ['ignoreErrors' => $ignoreErrors]];

src/Aop/Pointcut.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public function getKind(): int;
8282
public function matches(
8383
ReflectionClass|ReflectionFileNamespace $context,
8484
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null,
85-
object|string $instanceOrScope = null,
86-
array $arguments = null
85+
null|object|string $instanceOrScope = null,
86+
?array $arguments = null
8787
): bool;
8888
}

src/Aop/Pointcut/AndPointcut.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
/**
4040
* And constructor
4141
*/
42-
public function __construct(int $pointcutKind = null, Pointcut ...$pointcuts)
42+
public function __construct(?int $pointcutKind = null, Pointcut ...$pointcuts)
4343
{
4444
// If we don't have specified kind, it will be calculated as intersection then
4545
if (!isset($pointcutKind)) {
@@ -55,8 +55,8 @@ public function __construct(int $pointcutKind = null, Pointcut ...$pointcuts)
5555
public function matches(
5656
ReflectionClass|ReflectionFileNamespace $context,
5757
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null,
58-
object|string $instanceOrScope = null,
59-
array $arguments = null
58+
null|object|string $instanceOrScope = null,
59+
?array $arguments = null
6060
): bool {
6161
foreach ($this->pointcuts as $singlePointcut) {
6262
if (!$singlePointcut->matches($context, $reflector, $instanceOrScope, $arguments)) {

src/Aop/Pointcut/AttributePointcut.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ public function __construct(
4444
final public function matches(
4545
ReflectionClass|ReflectionFileNamespace $context,
4646
ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector = null,
47-
object|string $instanceOrScope = null,
48-
array $arguments = null
47+
null|object|string $instanceOrScope = null,
48+
?array $arguments = null
4949
): bool {
5050
// If we don't use context for matching and we do static check, then always match
5151
if (!$this->useContextForMatching && !isset($reflector)) {

0 commit comments

Comments
 (0)