Completed
Pull Request — master (#463)
by Alexander
30:17 queued 05:15
created

ClassProxyGenerator   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 210
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 8

Test Coverage

Coverage 95.4%

Importance

Changes 0
Metric Value
wmc 23
lcom 2
cbo 8
dl 0
loc 210
rs 10
c 0
b 0
f 0
ccs 83
cts 87
cp 0.954
1
<?php
2
3
declare(strict_types = 1);
4
/*
5
 * Go! AOP framework
6
 *
7
 * @copyright Copyright 2012, Lisachenko Alexander <[email protected]>
8
 *
9
 * This source file is subject to the license that is bundled
10
 * with this source code in the file LICENSE.
11
 */
12
13
namespace Go\Proxy;
14
15
use Go\Aop\Advice;
16
use Go\Aop\Framework\ClassFieldAccess;
17
use Go\Aop\Framework\DynamicClosureMethodInvocation;
18
use Go\Aop\Framework\ReflectionConstructorInvocation;
19
use Go\Aop\Framework\StaticClosureMethodInvocation;
20
use Go\Aop\Framework\StaticInitializationJoinpoint;
21
use Go\Aop\Intercept\Joinpoint;
22
use Go\Aop\Proxy;
23
use Go\Core\AspectContainer;
24
use Go\Core\AspectKernel;
25
use Go\Core\LazyAdvisorAccessor;
26
use Go\Proxy\Part\FunctionCallArgumentListGenerator;
27
use Go\Proxy\Part\InterceptedConstructorGenerator;
28
use Go\Proxy\Part\InterceptedMethodGenerator;
29
use Go\Proxy\Part\JoinPointPropertyGenerator;
30
use Go\Proxy\Part\PropertyInterceptionTrait;
31
use Laminas\Code\Generator\ClassGenerator;
32
use Laminas\Code\Generator\DocBlockGenerator;
33
use Laminas\Code\Reflection\DocBlockReflection;
34
use ReflectionClass;
35
use ReflectionMethod;
36
use ReflectionNamedType;
37
use UnexpectedValueException;
38
39
/**
40
 * Class proxy builder that is used to generate a child class from the list of joinpoints
41
 */
42
class ClassProxyGenerator
43
{
44
    /**
45
     * Static mappings for class name for excluding if..else check
46
     */
47
    protected static array $invocationClassMap = [
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_ARRAY, expecting T_FUNCTION or T_CONST
Loading history...
48
        AspectContainer::METHOD_PREFIX        => DynamicClosureMethodInvocation::class,
49
        AspectContainer::STATIC_METHOD_PREFIX => StaticClosureMethodInvocation::class,
50
        AspectContainer::PROPERTY_PREFIX      => ClassFieldAccess::class,
51
        AspectContainer::STATIC_INIT_PREFIX   => StaticInitializationJoinpoint::class,
52
        AspectContainer::INIT_PREFIX          => ReflectionConstructorInvocation::class
53
    ];
54
55
    /**
56
     * List of advices that are used for generation of child
57
     */
58
    protected array $adviceNames = [];
59
60
    /**
61
     * Instance of class generator
62
     */
63
    protected ClassGenerator $generator;
64
65
    /**
66
     * Should parameter widening be used or not
67
     */
68
    protected bool $useParameterWidening;
69
70 9
    /**
71
     * Generates an child code by original class reflection and joinpoints for it
72
     *
73
     * @param ReflectionClass $originalClass        Original class reflection
74
     * @param string          $parentClassName      Parent class name to use
75
     * @param string[][]      $classAdviceNames     List of advices for class
76 9
     * @param bool            $useParameterWidening Enables usage of parameter widening feature
77 9
     */
78 9
    public function __construct(
79 9
        ReflectionClass $originalClass,
80 9
        string $parentClassName,
81 9
        array $classAdviceNames,
82 9
        bool $useParameterWidening
83 9
    ) {
84
        $this->adviceNames          = $classAdviceNames;
85 9
        $this->useParameterWidening = $useParameterWidening;
86 9
87
        $dynamicMethodAdvices  = $classAdviceNames[AspectContainer::METHOD_PREFIX] ?? [];
88 9
        $staticMethodAdvices   = $classAdviceNames[AspectContainer::STATIC_METHOD_PREFIX] ?? [];
89
        $propertyAdvices       = $classAdviceNames[AspectContainer::PROPERTY_PREFIX] ?? [];
90 9
        $interceptedMethods    = array_keys($dynamicMethodAdvices + $staticMethodAdvices);
91 1
        $interceptedProperties = array_keys($propertyAdvices);
92 1
        $introducedInterfaces  = $classAdviceNames[AspectContainer::INTRODUCTION_INTERFACE_PREFIX] ?? [];
93 1
        $introducedTraits      = $classAdviceNames[AspectContainer::INTRODUCTION_TRAIT_PREFIX] ?? [];
94 1
95 1
        $generatedProperties = [new JoinPointPropertyGenerator($classAdviceNames)];
96
        $generatedMethods    = $this->interceptMethods($originalClass, $interceptedMethods);
97 1
98
        $introducedInterfaces[] = '\\' . Proxy::class;
99
100 9
        if (!empty($interceptedProperties)) {
101 9
            $generatedMethods['__construct'] = new InterceptedConstructorGenerator(
102 9
                $interceptedProperties,
103 9
                $originalClass->getConstructor(),
104 9
                $generatedMethods['__construct'] ?? null,
105 9
                $useParameterWidening
106 9
            );
107 9
            $introducedTraits[] = '\\' . PropertyInterceptionTrait::class;
108
        }
109 9
110
        $this->generator = new ClassGenerator(
111
            $originalClass->getShortName(),
112
            $originalClass->getNamespaceName(),
113
            $originalClass->isFinal() ? ClassGenerator::FLAG_FINAL : null,
114 9
            $parentClassName,
115 9
            $introducedInterfaces,
116
            $generatedProperties,
117
            $generatedMethods
118
        );
119
        if ($originalClass->getDocComment()) {
120 1
            $reflectionDocBlock = new DocBlockReflection($originalClass->getDocComment());
121
            $this->generator->setDocBlock(DocBlockGenerator::fromReflection($reflectionDocBlock));
122 1
        }
123 1
124
        $this->generator->addTraits($introducedTraits);
125
    }
126
127
    /**
128
     * Adds use alias for this class
129
     */
130 1
    public function addUse(string $use, string $useAlias = null): void
131
    {
132 1
        $this->generator->addUse($use, $useAlias);
133 1
    }
134
135 1
    /**
136 1
     * Inject advices into given class
137 1
     *
138 1
     * NB This method will be used as a callback during source code evaluation to inject joinpoints
139
     */
140 1
    public static function injectJoinPoints(string $targetClassName): void
141 1
    {
142 1
        $reflectionClass    = new ReflectionClass($targetClassName);
143
        $joinPointsProperty = $reflectionClass->getProperty(JoinPointPropertyGenerator::NAME);
144 1
145
        $joinPointsProperty->setAccessible(true);
146
        $advices    = $joinPointsProperty->getValue();
147
        $joinPoints = static::wrapWithJoinPoints($advices, $reflectionClass->getParentClass()->name);
148
        $joinPointsProperty->setValue($joinPoints);
149 9
150
        $staticInit = AspectContainer::STATIC_INIT_PREFIX . ':root';
151 9
        if (isset($joinPoints[$staticInit])) {
152
            ($joinPoints[$staticInit])();
153
        }
154
    }
155 9
156
    /**
157
     * Generates the source code of child class
158
     */
159
    public function generate(): string
160
    {
161
        $classCode = $this->generator->generate();
162
163
        return $classCode
164
            // Inject advices on call
165
            . '\\' . self::class . '::injectJoinPoints(' . $this->generator->getName() . '::class);';
166
    }
167
168
    /**
169 1
     * Wrap advices with joinpoint object
170
     *
171
     * @param array|Advice[][][] $classAdvices Advices for specific class
172 1
     *
173
     * @throws UnexpectedValueException If joinPoint type is unknown
174 1
     *
175 1
     * NB: Extension should be responsible for wrapping advice with join point.
176 1
     *
177
     * @return Joinpoint[] returns list of joinpoint ready to use
178
     */
179 1
    protected static function wrapWithJoinPoints(array $classAdvices, string $className): array
180
    {
181 1
        /** @var ?LazyAdvisorAccessor $accessor */
182
        static $accessor = null;
183 1
184
        if (!isset($accessor)) {
185
            $aspectKernel = AspectKernel::getInstance();
186 1
            $accessor     = $aspectKernel->getContainer()->get('aspect.advisor.accessor');
187 1
        }
188 1
189 1
        $joinPoints = [];
190
191
        foreach ($classAdvices as $joinPointType => $typedAdvices) {
192 1
            // if not isset then we don't want to create such invocation for class
193 1
            if (!isset(self::$invocationClassMap[$joinPointType])) {
194
                continue;
195
            }
196
            foreach ($typedAdvices as $joinPointName => $advices) {
197 1
                $filledAdvices = [];
198
                foreach ($advices as $advisorName) {
199
                    $filledAdvices[] = $accessor->$advisorName;
200
                }
201
202
                $joinpoint = new self::$invocationClassMap[$joinPointType]($filledAdvices, $className, $joinPointName);
203
                $joinPoints["$joinPointType:$joinPointName"] = $joinpoint;
204
            }
205
        }
206
207 9
        return $joinPoints;
208
    }
209 9
210 9
    /**
211 9
     * Returns list of intercepted method generators for class by method names
212 9
     *
213
     * @param string[] $methodNames List of methods to intercept
214 9
     *
215
     * @return InterceptedMethodGenerator[]
216
     */
217 9
    protected function interceptMethods(ReflectionClass $originalClass, array $methodNames): array
218
    {
219
        $interceptedMethods = [];
220
        foreach ($methodNames as $methodName) {
221
            $reflectionMethod = $originalClass->getMethod($methodName);
222
            $methodBody       = $this->getJoinpointInvocationBody($reflectionMethod);
223 9
224
            $interceptedMethods[$methodName] = new InterceptedMethodGenerator(
225 9
                $reflectionMethod,
226 9
                $methodBody,
227 9
                $this->useParameterWidening
228
            );
229 9
        }
230 9
231 9
        return $interceptedMethods;
232 9
    }
233 1
234 1
    /**
235
     * Creates string definition for method body by method reflection
236
     */
237
    protected function getJoinpointInvocationBody(ReflectionMethod $method): string
238
    {
239
        $isStatic = $method->isStatic();
240 9
        $scope    = $isStatic ? 'static::class' : '$this';
241 4
        $prefix   = $isStatic ? AspectContainer::STATIC_METHOD_PREFIX : AspectContainer::METHOD_PREFIX;
242
243
        $argumentList = new FunctionCallArgumentListGenerator($method);
244 9
        $argumentCode = $argumentList->generate();
245
        $return       = 'return ';
246 9
        if ($method->hasReturnType()) {
247
            $returnType = $method->getReturnType();
248
            if ($returnType instanceof ReflectionNamedType && $returnType->getName() === 'void') {
249
                // void return types should not return anything
250
                $return = '';
251
            }
252
        }
253
254
        if (!empty($argumentCode)) {
255
            $scope = "$scope, $argumentCode";
256
        }
257
258
        $body = "{$return}self::\$__joinPoints['{$prefix}:{$method->name}']->__invoke($scope);";
259
260
        return $body;
261
    }
262
}
263