Skip to content

Commit

Permalink
Merge pull request #214 from ray-di/pecl
Browse files Browse the repository at this point in the history
PECL extension support and introduce newly refined main class "Aspect"
  • Loading branch information
koriym committed Jul 7, 2024
2 parents 941db40 + d844ceb commit 28f4a78
Show file tree
Hide file tree
Showing 17 changed files with 756 additions and 228 deletions.
114 changes: 114 additions & 0 deletions .github/workflows/continuous-integration-pecl.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
name: Test with Ray.Aop PECL Extension

on:
push:
pull_request:
workflow_dispatch:

jobs:
build-and-test:
runs-on: ubuntu-latest

strategy:
matrix:
php-version: ['8.1', '8.2', '8.3']

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up PHP ${{ matrix.php-version }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: mbstring, intl
tools: phpize, composer:v2

- name: Install build tools and Valgrind
run: |
sudo apt-get update
sudo apt-get install -y autoconf automake libtool bison re2c valgrind
- name: Install Composer dependencies
run: composer install --prefer-dist --no-progress

- name: Build extension
id: build_extension
run: |
git clone https://github.com/ray-di/ext-rayaop.git
cd ext-rayaop
phpize
./configure
make
- name: PHP info
id: php_info
run: |
php -dextension=./ext-rayaop/modules/rayaop.so -i | grep rayaop
- name: Run PECL demo with debug logging
id: run_pecl_demo
run: |
timeout 60s php -n -dextension=./ext-rayaop/modules/rayaop.so -dmemory_limit=128M -dreport_memleaks=1 -dzend.assertions=1 -dassert.exception=1 ./ext-rayaop/smoke.php
continue-on-error: true

- name: Check loaded extension
id: check_loaded_extension
run: |
php -dextension=./ext-rayaop/modules/rayaop.so -r 'var_dump(extension_loaded("rayaop"));'
- name: Check function exists
id: check_function_exists
run: |
php -dextension=./ext-rayaop/modules/rayaop.so -r 'var_dump(function_exists("method_intercept"));'
- name: Run PHPUnit tests
id: run_phpunit_tests
run: |
php -dextension=./ext-rayaop/modules/rayaop.so vendor/bin/phpunit --coverage-clover=coverage.xml
- name: Run Valgrind memory check
if: steps.build_extension.outcome == 'failure' || steps.run_pecl_demo.outcome == 'failure'
run: |
cat << EOF > valgrind.supp
{
<insert_a_suppression_name_here>
Memcheck:Leak
match-leak-kinds: reachable
...
fun:php_module_startup
...
}
EOF
valgrind --suppressions=valgrind.supp --leak-check=full --show-leak-kinds=all --track-origins=yes --verbose --log-file=valgrind-out.txt php -n -dextension=./ext-rayaop/modules/rayaop.so vendor/bin/phpunit
- name: Check Valgrind results
if: steps.build_extension.outcome == 'failure' || steps.run_pecl_demo.outcome == 'failure'
run: |
if [ -f valgrind-out.txt ]; then
echo "Valgrind log found:"
cat valgrind-out.txt
if ! grep -q "ERROR SUMMARY: 0 errors from 0 contexts" valgrind-out.txt; then
echo "Valgrind found errors"
exit 1
fi
else
echo "Valgrind log not found. This is unexpected."
exit 1
fi
- name: Upload Valgrind log file
if: steps.build_extension.outcome == 'failure' || steps.run_pecl_demo.outcome == 'failure'
uses: actions/upload-artifact@v2
with:
name: valgrind-log
path: valgrind-out.txt
if-no-files-found: warn

- name: Final status check
if: always()
run: |
if [[ "${{ steps.build_extension.outcome }}" == 'failure' || "${{ steps.run_pecl_demo.outcome }}" == 'failure' ]]; then
echo "Build extension and run failed. Please check the logs for more information."
exit 1
fi
2 changes: 2 additions & 0 deletions .github/workflows/static-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ jobs:
uses: ray-di/.github/.github/workflows/static-analysis.yml@v1
with:
php_version: 8.2
has_crc_config: true

187 changes: 65 additions & 122 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,19 +75,18 @@ class WeekendBlocker implements MethodInterceptor
use Ray\Aop\Sample\Annotation\NotOnWeekends;
use Ray\Aop\Sample\Annotation\RealBillingService;

$pointcut = new Pointcut(
$aspect = new Aspect();
$aspect->bind(
(new Matcher())->any(),
(new Matcher())->annotatedWith(NotOnWeekends::class),
[new WeekendBlocker()]
);
$bind = (new Bind())->bind(RealBillingService::class, [$pointcut]);
$billing = (new Weaver($bind, $tmpDir))->newInstance(RealBillingService::class, [], $bind);

$billing = $aspect->newInstance(RealBillingService::class);
try {
echo $billing->chargeOrder();
} catch (\RuntimeException $e) {
echo $e->getMessage() . "\n";
exit(1);
}
```

Expand All @@ -97,117 +96,41 @@ try {
chargeOrder not allowed on weekends!
```

## メソッド名を指定したマッチ
## PECL拡張

```php
<?php
$bind = (new Bind())->bindInterceptors('chargeOrder', [new WeekendBlocker()]);
$compiler = new Weaver($bind, $tmpDir);
$billing = $compiler->newInstance('RealBillingService', [], $bind);
try {
echo $billing->chargeOrder();
} catch (\RuntimeException $e) {
echo $e->getMessage() . "\n";
exit(1);
}
```

## 独自のマッチャー

独自のマッチャーを作成することもでます。
`contains` マッチャーを作成するためには、2つのメソッドを持つクラスを提供する必要があります。
1つはクラスのマッチを行う`matchesClass`メソッド、もう1つはメソッドのマッチを行う`matchesMethod`です。いずれもマッチしたかどうかをブールで返します。

```php
use Ray\Aop\AbstractMatcher;

class IsContainsMatcher extends AbstractMatcher
{
/**
* {@inheritdoc}
*/
public function matchesClass(\ReflectionClass $class, array $arguments) : bool
{
[$contains] = $arguments;

return (strpos($class->name, $contains) !== false);
}

/**
* {@inheritdoc}
*/
public function matchesMethod(\ReflectionMethod $method, array $arguments) : bool
{
[$contains] = $arguments;

return (strpos($method->name, $contains) !== false);
}
}
```
Ray.Aopは[PECL拡張](https://github.com/ray-di/ext-rayaop)もサポートしています。拡張機能がインストールされている場合、weaveメソッドを使用してディレクトリ内のすべてのクラスにアスペクトを適用できます。

```php
$pointcut = new Pointcut(
$aspect = new Aspect();
$aspect->bind(
(new Matcher())->any(),
new IsContainsMatcher('charge'),
(new Matcher())->annotatedWith(NotOnWeekends::class),
[new WeekendBlocker()]
);
$bind = (new Bind)->bind(RealBillingService::class, [$pointcut]);
$billing = (new Weaver($bind, $tmpDir))->newInstance(RealBillingService::class, [$arg1, $arg2]);
$aspect->weave(__DIR__ . '/src'); // ディレクトリ内でマッチャーに一致するすべてのクラスにアスペクトを織り込みます。
$billing = new RealBillingService();
echo $billing->chargeOrder(); // インターセプターが適用されます。
```

マッチャーはdoctrineアノテーション、またはPHP8アトリビュートの読み込みをサポートします。
PECL拡張機能を使用すると:

```php
public function matchesClass(\ReflectionClass $class, array $arguments) : bool
{
assert($class instanceof \Ray\Aop\ReflectionClass);
$classAnnotation = $class->getAnnotation(Foo::class); // @Foo or #[Foo]
// ...
}
通常のnewキーワードを使用してコードのどこでも新しいインスタンスを作成できます。
インターセプションはfinalクラスやメソッドでも動作します。
これらの機能を使用するには、PECL拡張機能をインストールするだけで、Ray.Aopが自動的に利用します。

public function matchesMethod(\ReflectionMethod $method, array $arguments) : bool
{
assert($method instanceof \Ray\Aop\ReflectionMethod);
$methodAnnotation = $method->getAnnotation(Bar::class);
}
```
### PECL拡張のインストール

## パフォーマンス
PECL拡張機能の利用にはPHP 8.1以上が必要です。詳細は[ext-rayaop](https://github.com/ray-di/ext-rayaop?tab=readme-ov-file#installation)を参照してください。

`Weaver`オブジェクトはキャッシュ可能です。コンパイル、束縛、アノテーション読み込みコストを削減します。
## 設定オプション
Aspectインスタンスを作成する際に、オプションで一時ディレクトリを指定できます。

```php
$weaver = unserialize(file_get_contentes('./serializedWeaver'));
$billing = (new Weaver($bind, $tmpDir))->newInstance(RealBillingService::class, [$arg1, $arg2]);
$aspect = new Aspect('/path/to/tmp/dir');
```
指定しない場合、システムのデフォルトの一時ディレクトリが使用されます。

## 優先順位

インターセプターの実行順は以下のルールで決定されます。

* 基本的にはバインドした順に実行されます。
* `PriorityPointcut`で定義したものが最も優先されます。
* アノテーションでメソッドマッチするものは`PriorityPointcut`の次に優先されます。その時アノテートされた順で優先されます。

```php
/**
* @Auth // 1st
* @Cache // 2nd
* @Log // 3rd
*/
```

## 制限

この機能の背後ではメソッドのインターセプションを事前にコードを生成する事で可能にしています。Ray.Aopはダイナミックにサブクラスを生成してメソッドをオーバーライドするインターセプターを適用します。

クラスとメソッドは以下のものである必要があります。

* クラスは *final* ではない
* メソッドは *public*


## インターセプター
## インターセプターの詳細

呼び出されたメソッドをそのまま実行するだけのインターセプターは以下のようになります。

Expand All @@ -216,21 +139,25 @@ class MyInterceptor implements MethodInterceptor
{
public function invoke(MethodInvocation $invocation)
{
// メソッド実行前
//

// メソッド実行
$result = invocation->proceed();

// メソッド実行後
//

// メソッド呼び出し前
$result = $invocation->proceed();
// メソッド呼び出し後
return $result;
}
}
```

インターセプターに渡されるメソッド実行(`MethodInvocation`)オブジェクトは以下のメソッドを持ちます。
`$invocation->proceed()`はチェーン内の次のインターセプターを呼び出します。インターセプターが存在しない場合、ターゲットメソッドを呼び出します。このチェーンにより、単一のメソッドに対して複数のインターセプターを適用し、バインドされた順序で実行することができます。

インターセプターA、B、Cがメソッドにバインドされている場合の実行フローの例:

1. インターセプターA(前)
1. インターセプターB(前)
1. インターセプターC(前)
1. ターゲットメソッド
1. インターセプターC(後)
1. インターセプターB(後)
1. インターセプターA(後)


* [`$invocation->proceed()`](https://github.com/ray-di/Ray.Aop/blob/2.x/src/Joinpoint.php#L41) - メソッド実行
Expand All @@ -240,7 +167,7 @@ class MyInterceptor implements MethodInterceptor
* [`$invocation->getNamedArguments()`](https://github.com/ray-di/Ray.Aop/blob/2.x/src/Invocation.php#L32) - 名前付き引数の取得


拡張されたリフレクションはアノテーション取得のメソッドを持ちます
拡張されたClassReflectionとMethodReflectionはPHP 8の属性とドクトリンアノテーションを取得するメソッドを提供します

```php
/** @var $method \Ray\Aop\ReflectionMethod */
Expand All @@ -250,11 +177,37 @@ $class = $invocation->getMethod()->getDeclaringClass();
```


* [`$method->getAnnotations()`]() - メソッドアノテーションの取得
* [`$method->getAnnotations()`]() - メソッドアトリビュート/アノテーションの取得
* [`$method->getAnnotation($name)`]()
* [`$class->->getAnnotations()`]() - クラスアノテーションの取得
* [`$class->->getAnnotations()`]() - クラスアトリビュート/アノテーションの取得
* [`$class->->getAnnotation($name)`]()

## 独自のマッチャー

独自のマッチャーを作成できます。例えば、`ContainsMatcher`を作成するには:

```php
use Ray\Aop\AbstractMatcher;

class ContainsMatcher extends AbstractMatcher
{
public function matchesClass(\ReflectionClass $class, array $arguments) : bool
{
[$contains] = $arguments;
return (strpos($class->name, $contains) !== false);
}

public function matchesMethod(\ReflectionMethod $method, array $arguments) : bool
{
[$contains] = $arguments;
return (strpos($method->name, $contains) !== false);
}
}
```

アノテーション/属性
Ray.Aopは[doctrine/annotation](https://github.com/doctrine/annotations)またはPHP 8の[Attributes](https://www.php.net/manual/en/language.attributes.overview.php)のどちらかも使用できます。

## AOPアライアンス

このメソッドインターセプターのAPIは[AOPアライアンス](http://aopalliance.sourceforge.net/doc/org/aopalliance/intercept/MethodInterceptor.html)の部分実装です。
Expand All @@ -268,16 +221,6 @@ Ray.Aopの推奨インストール方法は、[Composer](https://github.com/comp
$ composer require ray/aop ~2.0
```

## パフォーマンス

AOPクラスのコンパイルにより、Ray.Aopは高速に動作します。アノテーションの読み込みは初回コンパイル時のみなので、ランタイムのパフォーマンスに影響を与えません。開発段階や最初の実行時にも、ファイルのタイムスタンプを利用してPHPファイルがキャッシュされ、通常はアノテーション生成のコストを気にする必要はありませんが、アプリケーションのブートストラップでアノテーションリーダーの設定を行うことで、初回コンパイル時のパフォーマンスが向上します。特に大規模なアプリケーションでこの設定は役立ちます。

### APCu

```php
SevericeLocator::setReader(new PsrCachedReader(new Reader(), $apcuCache));
```

### アトリビュートのみ使用(推奨)

```php
Expand Down
Loading

0 comments on commit 28f4a78

Please sign in to comment.