AopCode::addMethods()   B
last analyzed

Complexity

Conditions 8
Paths 16

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 16
c 0
b 0
f 0
nc 16
nop 2
dl 0
loc 27
rs 8.4444
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Ray\Aop;
6
7
use Ray\Aop\Exception\InvalidSourceClassException;
8
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...
9
use ReflectionNamedType;
10
use ReflectionUnionType;
11
12
use function array_keys;
13
use function file_exists;
14
use function file_get_contents;
15
use function implode;
16
use function in_array;
17
use function preg_replace;
18
use function preg_replace_callback;
19
use function sprintf;
20
use function token_get_all;
21
22
use const T_CLASS;
23
use const T_EXTENDS;
24
use const T_STRING;
25
26
final class AopCode
27
{
28
    public const INTERCEPT_STATEMENT = '\$this->_intercept(__FUNCTION__, func_get_args());';
29
30
    /** @var string */
31
    private $code = '';
32
33
    /** @var int  */
34
    private $curlyBraceCount = 0;
35
36
    /** @var MethodSignatureString */
37
    private $methodSignature;
38
39
    public function __construct(MethodSignatureString $methodSignature)
40
    {
41
        $this->methodSignature = $methodSignature;
42
    }
43
44
    /** @param ReflectionClass<object> $sourceClass */
45
    public function generate(ReflectionClass $sourceClass, BindInterface $bind, string $postfix): string
46
    {
47
        $this->parseClass($sourceClass, $postfix);
48
        $this->implementsInterface(WeavedInterface::class);
49
        $this->addMethods($sourceClass, $bind);
50
51
        return $this->getCodeText();
52
    }
53
54
    /** @return void */
55
    private function add(string $text)
56
    {
57
        if ($text === '{') {
58
            $this->curlyBraceCount++;
59
        }
60
61
        if ($text === '}') {
62
            // @codeCoverageIgnoreStart
63
            $this->curlyBraceCount--;
64
            // @codeCoverageIgnoreEnd
65
        }
66
67
        $this->code .= $text;
68
    }
69
70
    /** @param  non-empty-string $code */
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
71
    private function insert(string $code): void
72
    {
73
        $replacement = $code . '}';
74
        $this->code = (string) preg_replace('/}\s*$/', $replacement, $this->code);
75
    }
76
77
    private function addClassName(string $className, string $postfix): void
78
    {
79
        $newClassName = $className . $postfix;
80
        $this->add($newClassName . ' extends ' . $className . ' ');
81
    }
82
83
    /** @param ReflectionClass<object> $sourceClass */
84
    private function parseClass(ReflectionClass $sourceClass, string $postfix): void
85
    {
86
        $fileName = (string) $sourceClass->getFileName();
87
        if (! file_exists($fileName)) {
88
            throw new InvalidSourceClassException($sourceClass->getName());
89
        }
90
91
        $code = (string) file_get_contents($fileName);
92
        /** @var array<int, array{int, string, int}|string> $tokens */
93
        $tokens = token_get_all($code);
94
        $iterator = new TokenIterator($tokens);
95
        $inClass = false;
96
        $className = '';
97
98
        for ($iterator->rewind(); $iterator->valid(); $iterator->next()) {
99
            [$id, $text] = $iterator->getToken();
100
            $isClassKeyword = $id === T_CLASS;
101
            if ($isClassKeyword) {
102
                $inClass = true;
103
                $this->add($text);
104
                continue;
105
            }
106
107
            $isClassName = $inClass && $id === T_STRING && empty($className);
0 ignored issues
show
introduced by
$inClass is of type mixed, thus it always evaluated to false.
Loading history...
108
            if ($isClassName) {
109
                $className = $text;
110
                $this->addClassName($className, $postfix);
111
                continue;
112
            }
113
114
            $isExtendsKeyword = $id === T_EXTENDS;
115
            if ($isExtendsKeyword) {
116
                $iterator->skipExtends();
117
                continue;
118
            }
119
120
            $isClassSignatureEnds = $inClass && $text === '{';
121
            if ($isClassSignatureEnds) {
122
                $this->addIntercepterTrait();
123
124
                return;
125
            }
126
127
            $this->add($text);
128
        }
129
    }
130
131
    private function implementsInterface(string $interfaceName): void
132
    {
133
        $pattern = '/(class\s+[\w\s]+extends\s+\w+)(?:\s+implements\s+(.+))?/';
134
        $this->code = (string) preg_replace_callback($pattern, static function ($matches) use ($interfaceName) {
135
            if (isset($matches[2])) {
136
                // 既に implements が存在する場合
137
                // $match[0] class  FakePhp8Types_test extends FakePhp8Types  implements FakeNullInterface, \Ray\Aop\FakeNullInterface1
138
                // $match[1] class  FakePhp8Types_test extends FakePhp8Types
139
                // $match[2] FakeNullInterface, \Ray\Aop\FakeNullInterface1
140
                return sprintf('%s implements %s, \%s', $matches[1], $matches[2], $interfaceName);
141
            }
142
143
            // implements が存在しない場合
144
            return sprintf('%s implements \%s', $matches[0], $interfaceName);
145
        }, $this->code);
146
    }
147
148
    /** @param ReflectionClass<object> $class */
149
    private function addMethods(ReflectionClass $class, BindInterface $bind): void
150
    {
151
        $bindings = array_keys($bind->getBindings());
152
153
        $parentMethods = $class->getMethods();
154
        $interceptedMethods = [];
155
        foreach ($parentMethods as $method) {
156
            if (! in_array($method->getName(), $bindings)) {
157
                continue;
158
            }
159
160
            $signature = $this->methodSignature->get($method);
161
            $isVoid = false;
162
            if ($method->hasReturnType() && (! $method->getReturnType() instanceof ReflectionUnionType)) {
163
                $nt = $method->getReturnType();
164
                $isVoid = $nt instanceof ReflectionNamedType && $nt->getName()  === 'void';
165
            }
166
167
            $return = $isVoid ? '' : 'return ';
168
            $interceptedMethods[] = sprintf("    %s\n    {\n        %s%s\n    }\n", $signature, $return, self::INTERCEPT_STATEMENT);
169
        }
170
171
        if (! $interceptedMethods) {
172
            return;
173
        }
174
175
        $this->insert(implode("\n", $interceptedMethods));
176
    }
177
178
    private function addIntercepterTrait(): void
179
    {
180
        $this->add(sprintf("{\n    use \%s;\n}\n", InterceptTrait::class));
181
    }
182
183
    private function getCodeText(): string
184
    {
185
        // close opened curly brace
186
        while ($this->curlyBraceCount !== 0) {
187
            $this->code .= '}';
188
            $this->curlyBraceCount--;
189
        }
190
191
        return $this->code;
192
    }
193
}
194