Aspect   A
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 191
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 69
c 3
b 0
f 0
dl 0
loc 191
rs 9.76
wmc 33

11 Methods

Rating   Name   Duplication   Size   Complexity  
A bind() 0 6 1
A weave() 0 8 2
A newInstanceWithPecl() 0 8 1
A newInstance() 0 9 3
A createBind() 0 20 5
A applyInterceptors() 0 13 4
A processClass() 0 16 5
A newInstanceWithPhp() 0 10 2
A scanAndCompile() 0 18 5
A __construct() 0 3 1
A getClassNameFromFile() 0 17 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Ray\Aop;
6
7
use RecursiveDirectoryIterator;
8
use RecursiveIteratorIterator;
9
use ReflectionClass;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Ray\Aop\ReflectionClass. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
10
use ReflectionMethod;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Ray\Aop\ReflectionMethod. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
11
use RuntimeException;
12
use SplFileInfo;
13
14
use function array_keys;
15
use function array_slice;
16
use function assert;
17
use function basename;
18
use function class_exists;
19
use function count;
20
use function end;
21
use function extension_loaded;
22
use function get_declared_classes;
23
use function method_intercept; // @phpstan-ignore-line
0 ignored issues
show
introduced by
The function method_intercept was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
24
use function strcasecmp;
25
use function sys_get_temp_dir;
26
27
final class Aspect
28
{
29
    /** @var string|null */
30
    private $tmpDir;
31
32
    /** @var array<array{classMatcher: AbstractMatcher, methodMatcher: AbstractMatcher, interceptors: array<MethodInterceptor>}> */
33
    private $matchers = [];
34
35
    /** @var array<string, array<string, array<array-key, MethodInterceptor>>> */
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, array<stri...y, MethodInterceptor>>> at position 10 could not be parsed: Unknown type name 'array-key' at position 10 in array<string, array<string, array<array-key, MethodInterceptor>>>.
Loading history...
36
    private $bound = [];
37
38
    public function __construct(?string $tmpDir = null)
39
    {
40
        $this->tmpDir = $tmpDir ?? sys_get_temp_dir();
41
    }
42
43
    /** @param array<MethodInterceptor> $interceptors */
44
    public function bind(AbstractMatcher $classMatcher, AbstractMatcher $methodMatcher, array $interceptors): void
45
    {
46
        $this->matchers[] = [
47
            'classMatcher' => $classMatcher,
48
            'methodMatcher' => $methodMatcher,
49
            'interceptors' => $interceptors,
50
        ];
51
    }
52
53
    public function weave(string $classDir): void
54
    {
55
        if (! extension_loaded('rayaop')) {
56
            throw new RuntimeException('Ray.Aop extension is not loaded. Cannot use weave() method.');
57
        }
58
59
        $this->scanAndCompile($classDir);
60
        $this->applyInterceptors();
61
    }
62
63
    private function scanAndCompile(string $classDir): void
64
    {
65
        $files = new RecursiveIteratorIterator(
66
            new RecursiveDirectoryIterator($classDir)
67
        );
68
69
        /** @var SplFileInfo[] $files */
70
        foreach ($files as $file) {
71
            if ($file->isDir() || $file->getExtension() !== 'php') {
72
                continue;
73
            }
74
75
            $className = $this->getClassNameFromFile($file->getPathname());
76
            if ($className === null) {
77
                continue;
78
            }
79
80
            $this->processClass($className);
81
        }
82
    }
83
84
    private function getClassNameFromFile(string $file): ?string
85
    {
86
        $declaredClasses = get_declared_classes();
87
        $previousCount = count($declaredClasses);
88
89
        /** @psalm-suppress UnresolvableInclude */
90
        require_once $file;
91
92
        $newClasses = array_slice(get_declared_classes(), $previousCount);
93
94
        foreach ($newClasses as $class) {
95
            if (strcasecmp(basename($file, '.php'), $class) === 0) {
96
                return $class;
97
            }
98
        }
99
100
        return $newClasses ? end($newClasses) : null;
101
    }
102
103
    private function processClass(string $className): void
104
    {
105
        assert(class_exists($className));
106
        $reflection = new ReflectionClass($className);
107
108
        foreach ($this->matchers as $matcher) {
109
            if (! $matcher['classMatcher']->matchesClass($reflection, $matcher['classMatcher']->getArguments())) {
110
                continue;
111
            }
112
113
            foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
114
                if (! $matcher['methodMatcher']->matchesMethod($method, $matcher['methodMatcher']->getArguments())) {
115
                    continue;
116
                }
117
118
                $this->bound[$className][$method->getName()] = $matcher['interceptors'];
119
            }
120
        }
121
    }
122
123
    private function applyInterceptors(): void
124
    {
125
        if (! extension_loaded('rayaop')) {
126
            throw new RuntimeException('Ray.Aop extension is not loaded');
127
        }
128
129
        $dispatcher = new PeclDispatcher($this->bound);
130
        foreach ($this->bound as $className => $methods) {
131
            $methodNames = array_keys($methods);
132
            foreach ($methodNames as $methodName) {
133
                assert($dispatcher instanceof MethodInterceptorInterface);
134
                /** @psalm-suppress UndefinedFunction PECL-created function */
135
                method_intercept($className, $methodName, $dispatcher); // @phpstan-ignore-line
0 ignored issues
show
Bug introduced by
The function method_intercept was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

135
                /** @scrutinizer ignore-call */ 
136
                method_intercept($className, $methodName, $dispatcher); // @phpstan-ignore-line
Loading history...
136
            }
137
        }
138
    }
139
140
    /**
141
     * @param class-string<T> $className
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
142
     * @param list<mixed>     $args
143
     *
144
     * @return T
145
     *
146
     * @template T of object
147
     */
148
    public function newInstance(string $className, array $args = []): object
149
    {
150
        $reflection = new ReflectionClass($className);
151
152
        if ($reflection->isFinal() && extension_loaded('rayaop')) {
153
            return $this->newInstanceWithPecl($className, $args);
154
        }
155
156
        return $this->newInstanceWithPhp($className, $args);
157
    }
158
159
    /**
160
     * @param class-string<T> $className
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
161
     * @param array<mixed>    $args
162
     *
163
     * @return T
164
     *
165
     * @template T of object
166
     */
167
    private function newInstanceWithPecl(string $className, array $args): object
168
    {
169
        /** @psalm-suppress MixedMethodCall */
170
        $instance = new $className(...$args);
171
        $this->processClass($className);
172
        $this->applyInterceptors();
173
174
        return $instance;
175
    }
176
177
    /**
178
     * @param class-string<T> $className
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
179
     * @param list<mixed>     $args
180
     *
181
     * @return T
182
     *
183
     * @template T of object
184
     */
185
    private function newInstanceWithPhp(string $className, array $args): object
186
    {
187
        if ($this->tmpDir === null) {
188
            throw new RuntimeException('Temporary directory is not set. It is required for PHP-based AOP.');
189
        }
190
191
        $bind = $this->createBind($className);
192
        $weaver = new Weaver($bind, $this->tmpDir);
193
194
        return $weaver->newInstance($className, $args);
195
    }
196
197
    /** @param class-string $className */
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
198
    private function createBind(string $className): Bind
199
    {
200
        $bind = new Bind();
201
        $reflection = new \Ray\Aop\ReflectionClass($className);
202
203
        foreach ($this->matchers as $matcher) {
204
            if (! $matcher['classMatcher']->matchesClass($reflection, $matcher['classMatcher']->getArguments())) {
205
                continue;
206
            }
207
208
            foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
209
                if (! $matcher['methodMatcher']->matchesMethod($method, $matcher['methodMatcher']->getArguments())) {
210
                    continue;
211
                }
212
213
                $bind->bindInterceptors($method->getName(), $matcher['interceptors']);
214
            }
215
        }
216
217
        return $bind;
218
    }
219
}
220