Completed
Pull Request — 2.x (#349)
by Alexander
02:20
created

ClassProxyGenerator::__construct()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 46
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 4.25

Importance

Changes 0
Metric Value
dl 0
loc 46
ccs 24
cts 32
cp 0.75
rs 8.6315
c 0
b 0
f 0
cc 4
eloc 35
nc 4
nop 4
crap 4.25
1
<?php
2
declare(strict_types = 1);
3
/*
4
 * Go! AOP framework
5
 *
6
 * @copyright Copyright 2012, Lisachenko Alexander <[email protected]>
7
 *
8
 * This source file is subject to the license that is bundled
9
 * with this source code in the file LICENSE.
10
 */
11
12
namespace Go\Proxy;
13
14
use Go\Aop\Advice;
15
use Go\Aop\Framework\ClassFieldAccess;
16
use Go\Aop\Framework\DynamicClosureMethodInvocation;
17
use Go\Aop\Framework\ReflectionConstructorInvocation;
18
use Go\Aop\Framework\StaticClosureMethodInvocation;
19
use Go\Aop\Framework\StaticInitializationJoinpoint;
20
use Go\Aop\Intercept\Joinpoint;
21
use Go\Aop\Proxy;
22
use Go\Core\AspectContainer;
23
use Go\Core\AspectKernel;
24
use Go\Core\LazyAdvisorAccessor;
25
use Go\Proxy\Part\FunctionCallArgumentListGenerator;
26
use Go\Proxy\Part\InterceptedConstructorGenerator;
27
use Go\Proxy\Part\InterceptedMethodGenerator;
28
use Go\Proxy\Part\JoinPointPropertyGenerator;
29
use Go\Proxy\Part\PropertyInterceptionTrait;
30
use ReflectionClass;
31
use ReflectionMethod;
32
use Zend\Code\Generator\ClassGenerator;
33
use Zend\Code\Generator\DocBlockGenerator;
34
use Zend\Code\Reflection\DocBlockReflection;
35
36
/**
37
 * Class proxy builder that is used to generate a child class from the list of joinpoints
38
 */
39
class ClassProxyGenerator
40
{
41
42
    /**
43
     * Static mappings for class name for excluding if..else check
44
     *
45
     * @var null|array
46
     */
47
    protected static $invocationClassMap = [
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
     * @var array
59
     */
60
    protected $advices = [];
61
62
    /**
63
     * @var ClassGenerator
64
     */
65
    protected $generator;
66
67
    /**
68
     * Generates an child code by original class reflection and joinpoints for it
69
     *
70
     * @param ReflectionClass $originalClass        Original class reflection
71
     * @param string          $parentClassName      Parent class name to use
72
     * @param string[][]      $classAdvices         List of advices for class
73
     * @param bool            $useParameterWidening Enables usage of parameter widening feature
74
     */
75 8
    public function __construct(
76
        ReflectionClass $originalClass,
77
        string $parentClassName,
78
        array $classAdvices,
79
        bool $useParameterWidening
80
    ) {
81 8
        $this->advices         = $classAdvices;
82 8
        $dynamicMethodAdvices  = $classAdvices[AspectContainer::METHOD_PREFIX] ?? [];
83 8
        $staticMethodAdvices   = $classAdvices[AspectContainer::STATIC_METHOD_PREFIX] ?? [];
84 8
        $propertyAdvices       = $classAdvices[AspectContainer::PROPERTY_PREFIX] ?? [];
85 8
        $interceptedMethods    = array_keys($dynamicMethodAdvices + $staticMethodAdvices);
86 8
        $interceptedProperties = array_keys($propertyAdvices);
87 8
        $introducedInterfaces  = $classAdvices[AspectContainer::INTRODUCTION_INTERFACE_PREFIX] ?? [];
88 8
        $introducedTraits      = $classAdvices[AspectContainer::INTRODUCTION_TRAIT_PREFIX] ?? [];
89
90 8
        $generatedProperties = [new JoinPointPropertyGenerator($classAdvices)];
91 8
        $generatedMethods    = $this->interceptMethods($originalClass, $interceptedMethods);
92
93 8
        $introducedInterfaces[] = '\\' . Proxy::class;
94
95 8
        if (!empty($interceptedProperties)) {
96
            $generatedMethods['__construct'] = new InterceptedConstructorGenerator(
97
                $interceptedProperties,
98
                $originalClass->getConstructor(),
99
                $generatedMethods['__construct'] ?? null,
100
                $useParameterWidening
101
            );
102
            $introducedTraits[] = '\\' . PropertyInterceptionTrait::class;
103
        }
104
105 8
        $this->generator = new ClassGenerator(
106 8
            $originalClass->getShortName(),
107 8
            $originalClass->getNamespaceName(),
108 8
            $originalClass->isFinal() ? ClassGenerator::FLAG_FINAL : null,
109 8
            $parentClassName,
110 8
            $introducedInterfaces,
111 8
            $generatedProperties,
112 8
            $generatedMethods
113
        );
114 8
        if ($originalClass->getDocComment()) {
115
            $reflectionDocBlock = new DocBlockReflection($originalClass->getDocComment());
116
            $this->generator->setDocBlock(DocBlockGenerator::fromReflection($reflectionDocBlock));
117
        }
118
119 8
        $this->generator->addTraits($introducedTraits);
120 8
    }
121
122
    /**
123
     * Adds use alias for this class
124
     *
125
     * @param string      $use Name to use
126
     * @param string|null $useAlias Alias for this class name or null if not set
127
     */
128
    public function addUse(string $use, string $useAlias = null)
129
    {
130
        $this->generator->addUse($use, $useAlias);
131
    }
132
133
    /**
134
     * Inject advices into given class
135
     *
136
     * NB This method will be used as a callback during source code evaluation to inject joinpoints
137
     *
138
     * @param string $className Aop child proxy class
139
     */
140
    public static function injectJoinPoints(string $className)
141
    {
142
        $reflectionClass    = new ReflectionClass($className);
143
        $joinPointsProperty = $reflectionClass->getProperty(JoinPointPropertyGenerator::NAME);
144
145
        $joinPointsProperty->setAccessible(true);
146
        $advices    = $joinPointsProperty->getValue();
147
        $joinPoints = static::wrapWithJoinPoints($advices, $reflectionClass->getParentClass()->name);
148
        $joinPointsProperty->setValue($joinPoints);
149
150
        $staticInit = AspectContainer::STATIC_INIT_PREFIX . ':root';
151
        if (isset($joinPoints[$staticInit])) {
152
            $joinPoints[$staticInit]->__invoke();
153
        }
154
    }
155
156
    /**
157
     * Generates the source code of child class
158
     */
159 8
    public function generate(): string
160
    {
161 8
        $classCode = $this->generator->generate();
162
163
        return $classCode
164
            // Inject advices on call
165 8
            . '\\' . __CLASS__ . '::injectJoinPoints(' . $this->generator->getName() . '::class);';
166
    }
167
168
    /**
169
     * Wrap advices with joinpoint object
170
     *
171
     * @param array|Advice[][][] $classAdvices Advices for specific class
172
     * @param string $className Name of the original class to use
173
     *
174
     * @throws \UnexpectedValueException If joinPoint type is unknown
175
     *
176
     * NB: Extension should be responsible for wrapping advice with join point.
177
     *
178
     * @return array|Joinpoint[] returns list of joinpoint ready to use
179
     */
180
    protected static function wrapWithJoinPoints(array $classAdvices, string $className): array
181
    {
182
        /** @var LazyAdvisorAccessor $accessor */
183
        static $accessor;
184
185
        if (!isset($accessor)) {
186
            $aspectKernel = AspectKernel::getInstance();
187
            $accessor     = $aspectKernel->getContainer()->get('aspect.advisor.accessor');
188
        }
189
190
        $joinPoints = [];
191
192
        foreach ($classAdvices as $joinPointType => $typedAdvices) {
193
            // if not isset then we don't want to create such invocation for class
194
            if (!isset(self::$invocationClassMap[$joinPointType])) {
195
                continue;
196
            }
197
            foreach ($typedAdvices as $joinPointName => $advices) {
198
                $filledAdvices = [];
199
                foreach ($advices as $advisorName) {
200
                    $filledAdvices[] = $accessor->$advisorName;
201
                }
202
203
                $joinpoint = new self::$invocationClassMap[$joinPointType]($className, $joinPointName, $filledAdvices);
204
                $joinPoints["$joinPointType:$joinPointName"] = $joinpoint;
205
            }
206
        }
207
208
        return $joinPoints;
209
    }
210
211
    /**
212
     * Returns list of intercepted methods
213
     *
214
     * @param ReflectionClass $originalClass Instance of original reflection
215
     * @param array           $methodNames List of methods to intercept
216
     *
217
     * @return InterceptedMethodGenerator[]
218
     */
219 8
    protected function interceptMethods(ReflectionClass $originalClass, array $methodNames): array
220
    {
221 8
        $interceptedMethods = [];
222 8
        foreach ($methodNames as $methodName) {
223 8
            $reflectionMethod = $originalClass->getMethod($methodName);
224 8
            $methodBody       = $this->getJoinpointInvocationBody($reflectionMethod);
225
226 8
            $interceptedMethods[$methodName] = new InterceptedMethodGenerator($reflectionMethod, $methodBody);
227
        }
228
229 8
        return $interceptedMethods;
230
    }
231
232
    /**
233
     * Creates definition for method body
234
     *
235
     * @param ReflectionMethod $method Method reflection
236
     *
237
     * @return string new method body
238
     */
239 8
    protected function getJoinpointInvocationBody(ReflectionMethod $method): string
240
    {
241 8
        $isStatic = $method->isStatic();
242 8
        $scope    = $isStatic ? 'static::class' : '$this';
243 8
        $prefix   = $isStatic ? AspectContainer::STATIC_METHOD_PREFIX : AspectContainer::METHOD_PREFIX;
244
245 8
        $argumentList = new FunctionCallArgumentListGenerator($method);
246 8
        $argumentCode = $argumentList->generate();
247 8
        $return = 'return ';
248 8
        if (PHP_VERSION_ID >= 70100 && $method->hasReturnType()) {
249
            $returnType = (string) $method->getReturnType();
250
            if ($returnType === 'void') {
251
                // void return types should not return anything
252
                $return = '';
253
            }
254
        }
255
256 8
        if (!empty($argumentCode)) {
257 4
            $scope = "$scope, $argumentCode";
258
        }
259
260 8
        $body = "{$return}self::\$__joinPoints['{$prefix}:{$method->name}']->__invoke($scope);";
261
262 8
        return $body;
263
    }
264
}
265