Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract PECL code from Aspect to PeclAspect #223

Merged
merged 55 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
bdd9651
Fix extension requirement typo in AspectTest
koriym Sep 14, 2024
98fb101
Remove PECL-based instance creation logic
koriym Nov 4, 2024
868b957
Remove runtime extension check in applyInterceptors.
koriym Nov 4, 2024
848f2ac
Add ClassName extraction utility and corresponding tests
koriym Nov 4, 2024
91050bb
Refactor code by removing unused functions
koriym Nov 4, 2024
4248913
Add Fake classes and tests for aspect weaving
koriym Nov 4, 2024
74a12e1
Refactor weaving logic to new AspectPecl class
koriym Nov 4, 2024
9598e5b
Refactor `rayaop` extension check placement
koriym Nov 4, 2024
2eb9885
Change NotWritableException to extend RuntimeException
koriym Nov 4, 2024
819846f
Add PHP and extension requirements to tests
koriym Nov 4, 2024
913b296
Add Codecov action for coverage reporting
koriym Nov 4, 2024
b35b751
Stop running 05-pecl.php in demo and add it to CI
koriym Nov 4, 2024
ec67460
Enable PHP 7.2 compat
koriym Nov 4, 2024
a41c17e
Remove redundant extension check in weave method
koriym Nov 4, 2024
40dd6a1
Remove redundant type annotations in ClassName.php
koriym Nov 4, 2024
d12eac1
Fix function_exists check for method_intercept usage
koriym Nov 4, 2024
01a867b
Refactor class name extraction logic with helper methods
koriym Nov 4, 2024
ef95161
Remove unused assert function import
koriym Nov 4, 2024
ffe1055
Simplify Composer installation steps
koriym Nov 4, 2024
4a24ed5
Refactor Fake class structure and update namespaces
koriym Nov 4, 2024
29adc3c
Update phpdocumentor/type-resolver to version 1.9.0
koriym Nov 4, 2024
d9f4176
fixup! Refactor Fake class structure and update namespaces
koriym Nov 4, 2024
3c7bdcc
Add type hint for tokens variable
koriym Nov 4, 2024
7dd2e0b
Exclude explicit assertion sniff in PHPCS config
koriym Nov 4, 2024
cf605e1
Add debugging output to display fully qualified class names
koriym Nov 4, 2024
b611c1c
Refactor tests to use FakeNonFinalClass
koriym Nov 4, 2024
7d7f597
Add test for class with declare statement
koriym Nov 4, 2024
52dc82c
Add debugging var_dump for token parsing
koriym Nov 4, 2024
344c3cc
Refactor namespace parsing for unified approach
koriym Nov 4, 2024
f332582
Refactor namespace parsing and improve token handling
koriym Nov 4, 2024
f538442
Add debug statements for token and FQN output
koriym Nov 4, 2024
3ccfe9b
Add debug statements for token and FQN output
koriym Nov 4, 2024
a331515
Refactor token parsing and add debugging
koriym Nov 4, 2024
e856dc3
Refactor class name extraction logic
koriym Nov 4, 2024
7a4a679
Add debug statement to log tokens
koriym Nov 4, 2024
7228740
Add debug statements for namespace parsing
koriym Nov 4, 2024
ab9f892
Refactor class extraction to use ClassList
koriym Nov 4, 2024
f6ace02
Add demo script for PECL usage in billing service
koriym Nov 4, 2024
1ef6e33
Refactor setup and class list logic, remove unused imports
koriym Nov 4, 2024
6879add
Add phpstan baseline configuration for error suppression
koriym Nov 4, 2024
11d730d
Add debug statements to ClassList
koriym Nov 4, 2024
94ccf71
Refactor namespace parsing logic in ClassList
koriym Nov 4, 2024
8db1da6
Refactor ClassList to improve namespace handling
koriym Nov 4, 2024
994079d
Refactor anonymous function in namespace collection
koriym Nov 4, 2024
62d9569
Refactor ClassList to remove token_get_all usage
koriym Nov 4, 2024
882e97b
Replace deprecated str_contains with strpos
koriym Nov 4, 2024
bcdee69
Remove unnecessary script execution
koriym Nov 4, 2024
fcc3e8f
Replace str_contains with strpos in ClassList.php
koriym Nov 4, 2024
e6989b4
Combine and optimize CI workflow steps
koriym Nov 4, 2024
0a64c0d
Update CI to enrich PHP extensions and improve log handling
koriym Nov 4, 2024
933f083
Refactor ClassList fetching and testing logic
koriym Nov 4, 2024
9a60866
Add demo script execution under Valgrind in CI workflow
koriym Nov 4, 2024
219fa30
Add code coverage ignore annotations for PECL extension code
koriym Nov 4, 2024
7633fd7
Refactor class existence check in ClassList
koriym Nov 4, 2024
02caaff
Rename method for clarity and enhance code coverage
koriym Nov 4, 2024
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
8 changes: 8 additions & 0 deletions .github/workflows/continuous-integration-pecl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ jobs:
run: |
php -dextension=./ext-rayaop/modules/rayaop.so vendor/bin/phpunit --coverage-clover=coverage.xml

- name: Upload coverage report
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}

- name: Run Valgrind memory check
if: steps.build_extension.outcome == 'failure' || steps.run_pecl_demo.outcome == 'failure'
run: |
Expand Down Expand Up @@ -115,3 +120,6 @@ jobs:
echo "Build extension and run failed. Please check the logs for more information."
exit 1
fi

- name: Run additional script
run: php 'demo/05-pecl.php'
1 change: 1 addition & 0 deletions demo/run.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
passthru('php ' . __DIR__ . '/02-matcher-bind.php');
passthru('php ' . __DIR__ . '/03-annotation-bind.php');
passthru('php ' . __DIR__ . '/04-my-matcher.php');
//passthru('php ' . __DIR__ . '/05-pecl.php');
193 changes: 11 additions & 182 deletions src/Aspect.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,13 @@

namespace Ray\Aop;

use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use Ray\Aop\Exception\NotWritableException;
use ReflectionClass;
use ReflectionMethod;
use RuntimeException;
use SplFileInfo;

use function array_keys;
use function array_slice;
use function assert;
use function basename;
use function class_exists;
use function count;
use function end;
use function extension_loaded;
use function get_declared_classes;
use function method_intercept; // @phpstan-ignore-line
use function strcasecmp;
use function file_exists;
use function is_writable;
use function sys_get_temp_dir;

/**
Expand All @@ -35,6 +24,7 @@
* methodMatcher: AbstractMatcher,
* interceptors: MethodInterceptors
* }
* @psalm-type MatcherConfigList = array<array-key, MatcherConfig>
* @psalm-type Arguments = array<array-key, mixed>
*/
final class Aspect
Expand All @@ -50,25 +40,20 @@ final class Aspect
/**
* Collection of matcher configurations
*
* @var array<array-key, MatcherConfig>
* @var MatcherConfigList
*/
private $matchers = [];

/**
* Bound interceptors map
*
* @var ClassBindings
*/
private $bound = [];

/** @param non-empty-string|null $tmpDir Directory for generated proxy classes */
public function __construct(?string $tmpDir = null)
{
if ($tmpDir === null) {
$tmp = sys_get_temp_dir();
$this->tmpDir = $tmp !== '' ? $tmp : '/tmp';
$tmpDir = $tmp !== '' ? $tmp : '/tmp';
}

return;
if (! file_exists($tmpDir) || ! is_writable($tmpDir)) {
throw new NotWritableException("{$tmpDir} is not writable.");
}

$this->tmpDir = $tmpDir;
Expand All @@ -95,124 +80,13 @@ public function bind(AbstractMatcher $classMatcher, AbstractMatcher $methodMatch
/**
* Weave aspects into classes in the specified directory
*
* @param string $classDir Target class directory
* @param non-empty-string $classDir Target class directory
*
* @throws RuntimeException When Ray.Aop extension is not loaded.
*
* @codeCoverageIgnore
*/
public function weave(string $classDir): void
{
if (! extension_loaded('rayaop')) {
throw new RuntimeException('Ray.Aop extension is not loaded. Cannot use weave() method.');
}

$this->scanDirectory($classDir);
$this->applyInterceptors();
}

/**
* Scan directory and compile classes
*
* @codeCoverageIgnore
*/
private function scanDirectory(string $classDir): void
{
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($classDir)
);

/** @var SplFileInfo[] $files */
foreach ($files as $file) {
if ($file->isDir() || $file->getExtension() !== 'php') {
continue;
}

$className = $this->getClassNameFromFile($file->getPathname());
if ($className === null) {
continue;
}

$this->processClass($className);
}
}

/**
* Get class name from file
*
* @return class-string|null
*
* @codeCoverageIgnore
*/
private function getClassNameFromFile(string $file): ?string
{
$declaredClasses = get_declared_classes();
$previousCount = count($declaredClasses);

/** @psalm-suppress UnresolvableInclude */
require_once $file;

$newClasses = array_slice(get_declared_classes(), $previousCount);

foreach ($newClasses as $class) {
if (strcasecmp(basename($file, '.php'), $class) === 0) {
return $class;
}
}

return $newClasses ? end($newClasses) : null;
}

/**
* Process class for interception
*
* @param class-string $className
*
* @codeCoverageIgnore
*/
private function processClass(string $className): void
{
assert(class_exists($className));
$reflection = new ReflectionClass($className);

foreach ($this->matchers as $matcher) {
if (! $matcher['classMatcher']->matchesClass($reflection, $matcher['classMatcher']->getArguments())) {
continue;
}

/** @var ReflectionMethod[] $methods */
$methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);
foreach ($methods as $method) {
if (! $matcher['methodMatcher']->matchesMethod($method, $matcher['methodMatcher']->getArguments())) {
continue;
}

$this->bound[$className][$method->getName()] = $matcher['interceptors'];
}
}
}

/**
* Apply interceptors to bound methods
*
* @codeCoverageIgnore
*/
private function applyInterceptors(): void
{
if (! extension_loaded('rayaop')) {
throw new RuntimeException('Ray.Aop extension is not loaded');
}

$dispatcher = new PeclDispatcher($this->bound);

foreach ($this->bound as $className => $methods) {
$methodNames = array_keys($methods);
foreach ($methodNames as $methodName) {
assert($dispatcher instanceof MethodInterceptorInterface);
/** @psalm-suppress UndefinedFunction */
method_intercept($className, $methodName, $dispatcher); // @phpstan-ignore-line
}
}
(new AspectPecl())->weave($classDir, $this->matchers);
}

/**
Expand All @@ -228,51 +102,6 @@ private function applyInterceptors(): void
* @template T of object
*/
public function newInstance(string $className, array $args = []): object
{
$reflection = new ReflectionClass($className);

if ($reflection->isFinal() && extension_loaded('rayaop')) {
return $this->newInstanceWithPecl($className, $args); // @codeCoverageIgnore
}

return $this->newInstanceWithPhp($className, $args);
}

/**
* Create instance using PECL extension
*
* @param class-string<T> $className
* @param Arguments $args
*
* @return T
*
* @template T of object
* @codeCoverageIgnore
*/
private function newInstanceWithPecl(string $className, array $args): object
{
/** @psalm-suppress MixedMethodCall */
$instance = new $className(...$args);
$this->processClass($className);
$this->applyInterceptors();

/** @var T $instance */
return $instance;
}

/**
* Create instance using PHP-based implementation
*
* @param class-string<T> $className
* @param list<mixed> $args
*
* @return T
*
* @throws RuntimeException When temporary directory is not set.
*
* @template T of object
*/
private function newInstanceWithPhp(string $className, array $args): object
{
$bind = $this->createBind($className);
$weaver = new Weaver($bind, $this->tmpDir);
Expand Down
124 changes: 124 additions & 0 deletions src/AspectPecl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php

declare(strict_types=1);

namespace Ray\Aop;

use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionClass;
use ReflectionMethod;
use RuntimeException;
use SplFileInfo;

use function array_keys;
use function assert;
use function class_exists;
use function extension_loaded;
use function function_exists;
use function method_intercept;

Check failure on line 19 in src/AspectPecl.php

View workflow job for this annotation

GitHub Actions / sa / PHPStan

Used function method_intercept not found.

/**
* @psalm-type MethodBoundInterceptors = array<non-empty-string, MethodInterceptors>
* @psalm-type ClassBoundInterceptors = array<class-string, MethodBoundInterceptors>
* @psalm-import-type MatcherConfigList from Aspect
* @psalm-import-type MethodInterceptors from Aspect
*/
final class AspectPecl
{
public function __construct()
{
if (! extension_loaded('rayaop')) {
throw new RuntimeException('Ray.Aop extension is not loaded. Cannot use weave() method.'); // @codeCoverageIgnore
}
}

/**
* Weave aspects into classes in the specified directory
*
* @param non-empty-string $classDir Target class directory
* @param MatcherConfigList $mathcers List of matchers and interceptors
*
* @throws RuntimeException When Ray.Aop extension is not loaded.
*/
public function weave(string $classDir, array $mathcers): void
{
if (! extension_loaded('rayaop')) {
throw new RuntimeException('Ray.Aop extension is not loaded. Cannot use weave() method.'); // @codeCoverageIgnore
}

$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($classDir)
);

/** @var SplFileInfo[] $files */
foreach ($files as $file) {
if ($file->isDir() || $file->getExtension() !== 'php') {
continue;
}

$className = ClassName::from($file->getPathname());

if ($className === null) {
continue;
}

assert(class_exists($className), $className);

$boundInterceptors = $this->getBoundInterceptors($className, $mathcers);
$this->applyInterceptors($boundInterceptors);
}
}

/**
* Process class for interception
*
* @param class-string $className
* @param MatcherConfigList $matchers
*
* @return ClassBoundInterceptors
*/
private function getBoundInterceptors(string $className, array $matchers): array
{
assert(class_exists($className), $className);
$reflection = new ReflectionClass($className);

$bound = [];
foreach ($matchers as $matcher) {
if (! $matcher['classMatcher']->matchesClass($reflection, $matcher['classMatcher']->getArguments())) {
continue;
}

/** @var ReflectionMethod[] $methods */
$methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);
foreach ($methods as $method) {
if (! $matcher['methodMatcher']->matchesMethod($method, $matcher['methodMatcher']->getArguments())) {
continue;
}

$bound[$className][$method->getName()] = $matcher['interceptors'];
}
}

return $bound;
}

/**
* Apply interceptors to bound methods
*
* @param ClassBoundInterceptors $boundInterceptors
*/
private function applyInterceptors(array $boundInterceptors): void
{
$dispatcher = new PeclDispatcher($boundInterceptors);
assert(function_exists('method_intercept')); // PECL Ray.Aop extension

foreach ($boundInterceptors as $className => $methods) {
$methodNames = array_keys($methods);
foreach ($methodNames as $methodName) {
assert($dispatcher instanceof MethodInterceptorInterface);
method_intercept($className, $methodName, $dispatcher);
}
}
}
}
Loading
Loading