Completed
Push — master ( 345cbf...06ef8e )
by Alexander
02:03
created

ClassProxy::__toString()   C

Complexity

Conditions 7
Paths 16

Size

Total Lines 29
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
dl 0
loc 29
ccs 0
cts 19
cp 0
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 21
nc 16
nop 0
crap 56
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\IntroductionInfo;
22
use Go\Aop\Proxy;
23
use Go\Core\AspectContainer;
24
use Go\Core\AspectKernel;
25
use Go\Core\LazyAdvisorAccessor;
26
use Reflection;
27
use ReflectionClass;
28
use ReflectionMethod;
29
use ReflectionProperty;
30
31
/**
32
 * Class proxy builder that is used to generate a child class from the list of joinpoints
33
 */
34
class ClassProxy extends AbstractProxy
35
{
36
    /**
37
     * Parent class reflection
38
     *
39
     * @var null|ReflectionClass
40
     */
41
    protected $class;
42
43
    /**
44
     * Parent class name, can be changed manually
45
     *
46
     * @var string
47
     */
48
    protected $parentClassName;
49
50
    /**
51
     * Source code for methods
52
     *
53
     * @var array Name of method => source code for it
54
     */
55
    protected $methodsCode = [];
56
57
    /**
58
     * Static mappings for class name for excluding if..else check
59
     *
60
     * @var null|array
61
     */
62
    protected static $invocationClassMap = [
63
        AspectContainer::METHOD_PREFIX        => DynamicClosureMethodInvocation::class,
64
        AspectContainer::STATIC_METHOD_PREFIX => StaticClosureMethodInvocation::class,
65
        AspectContainer::PROPERTY_PREFIX      => ClassFieldAccess::class,
66
        AspectContainer::STATIC_INIT_PREFIX   => StaticInitializationJoinpoint::class,
67
        AspectContainer::INIT_PREFIX          => ReflectionConstructorInvocation::class
68
    ];
69
70
    /**
71
     * List of additional interfaces to implement
72
     *
73
     * @var array
74
     */
75
    protected $interfaces = [];
76
77
    /**
78
     * List of additional traits for using
79
     *
80
     * @var array
81
     */
82
    protected $traits = [];
83
84
    /**
85
     * Source code for properties
86
     *
87
     * @var array Name of property => source code for it
88
     */
89
    protected $propertiesCode = [];
90
91
    /**
92
     * Name for the current class
93
     *
94
     * @var string
95
     */
96
    protected $name = '';
97
98
    /**
99
     * Flag to determine if we need to add a code for property interceptors
100
     *
101
     * @var bool
102
     */
103
    private $isFieldsIntercepted = false;
104
105
    /**
106
     * List of intercepted properties names
107
     *
108
     * @var array
109
     */
110
    private $interceptedProperties = [];
111
112
    /**
113
     * Generates an child code by parent class reflection and joinpoints for it
114
     *
115
     * @param ReflectionClass $parent Parent class reflection
116
     * @param array|Advice[][] $classAdvices List of advices for class
117
     *
118
     * @throws \InvalidArgumentException if there are unknown type of advices
119
     */
120 6
    public function __construct(ReflectionClass $parent, array $classAdvices)
121
    {
122 6
        parent::__construct($classAdvices);
123
124 6
        $this->class           = $parent;
125 6
        $this->name            = $parent->getShortName();
126 6
        $this->parentClassName = $parent->getShortName();
127
128 6
        $this->addInterface(Proxy::class);
129 6
        $this->addJoinpointsProperty();
130
131 6
        foreach ($classAdvices as $type => $typedAdvices) {
132
            switch ($type) {
133 6
                case AspectContainer::METHOD_PREFIX:
134
                case AspectContainer::STATIC_METHOD_PREFIX:
135 6
                    foreach ($typedAdvices as $joinPointName => $advice) {
136 6
                        $method = $parent->getMethod($joinPointName);
137 6
                        $this->overrideMethod($method);
138
                    }
139 6
                    break;
140
141
                case AspectContainer::PROPERTY_PREFIX:
142
                    foreach ($typedAdvices as $joinPointName => $advice) {
143
                        $property = $parent->getProperty($joinPointName);
144
                        $this->interceptProperty($property);
145
                    }
146
                    break;
147
148
                case AspectContainer::INTRODUCTION_TRAIT_PREFIX:
149
                    foreach ($typedAdvices as $advice) {
150
                        /** @var $advice IntroductionInfo */
151
                        $introducedTrait = $advice->getTrait();
152
                        if (!empty($introducedTrait)) {
153
                            $this->addTrait($introducedTrait);
154
                        }
155
                        $introducedInterface = $advice->getInterface();
156
                        if (!empty($introducedInterface)) {
157
                            $this->addInterface($introducedInterface);
158
                        }
159
                    }
160
                    break;
161
162
                case AspectContainer::INIT_PREFIX:
163
                case AspectContainer::STATIC_INIT_PREFIX:
164
                    break; // No changes for class
165
166
                default:
167 6
                    throw new \InvalidArgumentException("Unsupported point `$type`");
168
            }
169
        }
170 6
    }
171
172
173
    /**
174
     * Updates parent name for child
175
     *
176
     * @param string $newParentName New class name
177
     */
178 6
    public function setParentName(string $newParentName)
179
    {
180 6
        $this->parentClassName = $newParentName;
181 6
    }
182
183
    /**
184
     * Override parent method with new body
185
     *
186
     * @param string $methodName Method name to override
187
     * @param string $body New body for method
188
     */
189 6
    public function override(string $methodName, string $body)
190
    {
191 6
        $this->methodsCode[$methodName] = $this->getOverriddenFunction($this->class->getMethod($methodName), $body);
192 6
    }
193
194
    /**
195
     * Creates a method
196
     *
197
     * @param int $methodFlags See ReflectionMethod modifiers
198
     * @param string $methodName Name of the method
199
     * @param bool $byReference Is method should return value by reference
200
     * @param string $body Body of method
201
     * @param string $parameters Definition of parameters
202
     */
203
    public function setMethod(int $methodFlags, string $methodName, bool $byReference, string $body, string $parameters)
204
    {
205
        $this->methodsCode[$methodName] = (
206
            "/**\n * Method was created automatically, do not change it manually\n */\n" .
207
            implode(' ', Reflection::getModifierNames($methodFlags)) . // List of method modifiers
208
            ' function ' . // 'function' keyword
209
            ($byReference ? '&' : '') . // Return value by reference
210
            $methodName . // Method name
211
            '(' . // Start of parameter list
212
            $parameters . // List of parameters
213
            ")\n" . // End of parameter list
214
            "{\n" . // Start of method body
215
            $this->indent($body) . "\n" . // Method body
216
            "}\n" // End of method body
217
        );
218
    }
219
220
    /**
221
     * Inject advices into given class
222
     *
223
     * NB This method will be used as a callback during source code evaluation to inject joinpoints
224
     *
225
     * @param string $className Aop child proxy class
226
     */
227
    public static function injectJoinPoints(string $className)
228
    {
229
        $reflectionClass    = new ReflectionClass($className);
230
        $joinPointsProperty = $reflectionClass->getProperty('__joinPoints');
231
232
        $joinPointsProperty->setAccessible(true);
233
        $advices    = $joinPointsProperty->getValue();
234
        $joinPoints = static::wrapWithJoinPoints($advices, $reflectionClass->getParentClass()->name);
235
        $joinPointsProperty->setValue($joinPoints);
236
237
        $staticInit = AspectContainer::STATIC_INIT_PREFIX . ':root';
238
        if (isset($joinPoints[$staticInit])) {
239
            $joinPoints[$staticInit]->__invoke();
240
        }
241
    }
242
243
    /**
244
     * Wrap advices with joinpoint object
245
     *
246
     * @param array|Advice[] $classAdvices Advices for specific class
247
     * @param string $className Name of the original class to use
248
     *
249
     * @throws \UnexpectedValueException If joinPoint type is unknown
250
     *
251
     * NB: Extension should be responsible for wrapping advice with join point.
252
     *
253
     * @return array|Joinpoint[] returns list of joinpoint ready to use
254
     */
255
    protected static function wrapWithJoinPoints(array $classAdvices, string $className): array
256
    {
257
        /** @var LazyAdvisorAccessor $accessor */
258
        static $accessor;
259
260
        if (!isset($accessor)) {
261
            $aspectKernel = AspectKernel::getInstance();
262
            $accessor     = $aspectKernel->getContainer()->get('aspect.advisor.accessor');
263
        }
264
265
        $joinPoints = [];
266
267
        foreach ($classAdvices as $joinPointType => $typedAdvices) {
268
            // if not isset then we don't want to create such invocation for class
269
            if (!isset(self::$invocationClassMap[$joinPointType])) {
270
                continue;
271
            }
272
            foreach ($typedAdvices as $joinPointName => $advices) {
273
                $filledAdvices = [];
274
                foreach ($advices as $advisorName) {
275
                    $filledAdvices[] = $accessor->$advisorName;
276
                }
277
278
                $joinpoint = new self::$invocationClassMap[$joinPointType]($className, $joinPointName, $filledAdvices);
279
                $joinPoints["$joinPointType:$joinPointName"] = $joinpoint;
280
            }
281
        }
282
283
        return $joinPoints;
284
    }
285
286
    /**
287
     * Add an interface for child
288
     *
289
     * @param string $interfaceName Name of the interface to add
290
     */
291 6
    public function addInterface(string $interfaceName)
292
    {
293
        // Use absolute namespace to prevent NS-conflicts
294 6
        $this->interfaces[] = '\\' . ltrim($interfaceName, '\\');
295 6
    }
296
297
    /**
298
     * Add a trait for child
299
     *
300
     * @param string $traitName Name of the trait to add
301
     */
302
    public function addTrait(string $traitName)
303
    {
304
        // Use absolute namespace to prevent NS-conflicts
305
        $this->traits[] = '\\' . ltrim($traitName, '\\');
306
    }
307
308
    /**
309
     * Creates a property
310
     *
311
     * @param int $propFlags See ReflectionProperty modifiers
312
     * @param string $propName Name of the property
313
     * @param null|string $defaultText Default value, should be string text!
314
     */
315 6
    public function setProperty(int $propFlags, string $propName, string $defaultText = null)
316
    {
317 6
        $this->propertiesCode[$propName] = (
318
            "/**\n * Property was created automatically, do not change it manually\n */\n" . // Doc-block
319 6
            implode(' ', Reflection::getModifierNames($propFlags)) . // List of modifiers for property
320 6
            ' $' . // Space and variable symbol
321 6
            $propName . // Name of the property
322 6
            (isset($defaultText) ? " = $defaultText" : '') . // Default value if present
323 6
            ";\n" // End of line with property definition
324
        );
325 6
    }
326
327
    /**
328
     * Adds a definition for joinpoints private property in the class
329
     */
330 6
    protected function addJoinpointsProperty()
331
    {
332 6
        $exportedAdvices = strtr(json_encode($this->advices, JSON_PRETTY_PRINT), [
333 6
            '{' => '[',
334
            '}' => ']',
335
            '"' => '\'',
336
            ':' => ' =>'
337
        ]);
338 6
        $this->setProperty(
339 6
            ReflectionProperty::IS_PRIVATE | ReflectionProperty::IS_STATIC,
340 6
            '__joinPoints',
341
            $exportedAdvices
342
        );
343 6
    }
344
345
    /**
346
     * Override parent method with joinpoint invocation
347
     *
348
     * @param ReflectionMethod $method Method reflection
349
     */
350 6
    protected function overrideMethod(ReflectionMethod $method)
351
    {
352
        // temporary disable override of final methods
353 6
        if (!$method->isFinal() && !$method->isAbstract()) {
354 6
            $this->override($method->name, $this->getJoinpointInvocationBody($method));
355
        }
356 6
    }
357
358
    /**
359
     * Creates definition for method body
360
     *
361
     * @param ReflectionMethod $method Method reflection
362
     *
363
     * @return string new method body
364
     */
365 6
    protected function getJoinpointInvocationBody(ReflectionMethod $method): string
366
    {
367 6
        $isStatic = $method->isStatic();
368 6
        $scope    = $isStatic ? self::$staticLsbExpression : '$this';
369 6
        $prefix   = $isStatic ? AspectContainer::STATIC_METHOD_PREFIX : AspectContainer::METHOD_PREFIX;
370
371 6
        $args   = $this->prepareArgsLine($method);
372 6
        $return = 'return ';
373 6
        if (PHP_VERSION_ID >= 70100 && $method->hasReturnType()) {
374
            $returnType = (string) $method->getReturnType();
375
            if ($returnType === 'void') {
376
                // void return types should not return anything
377
                $return = '';
378
            }
379
        }
380
381 6
        if (!empty($args)) {
382 3
            $scope = "$scope, $args";
383
        }
384
385 6
        $body = "{$return}self::\$__joinPoints['{$prefix}:{$method->name}']->__invoke($scope);";
386
387 6
        return $body;
388
    }
389
390
    /**
391
     * Makes property intercepted
392
     *
393
     * @param ReflectionProperty $property Reflection of property to intercept
394
     */
395
    protected function interceptProperty(ReflectionProperty $property)
396
    {
397
        $this->interceptedProperties[] = is_object($property) ? $property->name : $property;
398
        $this->isFieldsIntercepted = true;
399
    }
400
401
    /**
402
     * {@inheritDoc}
403
     */
404
    public function __toString()
405
    {
406
        $ctor = $this->class->getConstructor();
407
        if ($this->isFieldsIntercepted && (!$ctor || !$ctor->isPrivate())) {
408
            $this->addFieldInterceptorsCode($ctor);
409
        }
410
411
        $prefix = implode(' ', Reflection::getModifierNames($this->class->getModifiers()));
412
413
        $classCode = (
414
            $this->class->getDocComment() . "\n" . // Original doc-block
415
            ($prefix ? "$prefix " : '') . // List of class modifiers
416
            'class ' . // 'class' keyword with one space
417
            $this->name . // Name of the class
418
            ' extends ' . // 'extends' keyword with
419
            $this->parentClassName . // Name of the parent class
420
            ($this->interfaces ? ' implements ' . implode(', ', $this->interfaces) : '') . "\n" . // Interfaces list
421
            "{\n" . // Start of class definition
422
            ($this->traits ? $this->indent('use ' . implode(', ', $this->traits) . ';' . "\n") : '') . "\n" . // Traits list
423
            $this->indent(implode("\n", $this->propertiesCode)) . "\n" . // Property definitions
424
            $this->indent(implode("\n", $this->methodsCode)) . "\n" . // Method definitions
425
            '}' // End of class definition
426
        );
427
428
        return $classCode
429
            // Inject advices on call
430
            . PHP_EOL
431
            . '\\' . __CLASS__ . '::injectJoinPoints(' . $this->class->getShortName() . '::class);';
432
    }
433
434
    /**
435
     * Add code for intercepting properties
436
     *
437
     * @param null|ReflectionMethod $constructor Constructor reflection or null
438
     */
439
    protected function addFieldInterceptorsCode(ReflectionMethod $constructor = null)
440
    {
441
        $this->addTrait(PropertyInterceptionTrait::class);
442
        $this->isFieldsIntercepted = true;
443
        if ($constructor) {
444
            $this->override('__construct', $this->getConstructorBody($constructor, true));
445
        } else {
446
            $this->setMethod(ReflectionMethod::IS_PUBLIC, '__construct', false, $this->getConstructorBody(), '');
447
        }
448
    }
449
450
    /**
451
     * Returns constructor code
452
     *
453
     * @param ReflectionMethod $constructor Constructor reflection
454
     * @param bool $isCallParent Is there is a need to call parent code
455
     *
456
     * @return string
457
     */
458
    private function getConstructorBody(ReflectionMethod $constructor = null, bool $isCallParent = false): string
459
    {
460
        $assocProperties = [];
461
        $listProperties  = [];
462
        foreach ($this->interceptedProperties as $propertyName) {
463
            $assocProperties[] = "'$propertyName' => &\$this->$propertyName";
464
            $listProperties[]  = "\$this->$propertyName";
465
        }
466
        $assocProperties = $this->indent(implode(',' . PHP_EOL, $assocProperties));
467
        $listProperties  = $this->indent(implode(',' . PHP_EOL, $listProperties));
468
        $parentCall      = '';
469
        if ($constructor !== null && isset($this->methodsCode['__construct'])) {
470
            $parentCall = $this->getJoinpointInvocationBody($constructor);
471
        } elseif ($isCallParent) {
472
            $parentCall = '\call_user_func_array(["parent", __FUNCTION__], \func_get_args());';
473
        }
474
475
        return <<<CTOR
476
\$this->__properties = array(
477
$assocProperties
478
);
479
unset(
480
$listProperties
481
);
482
$parentCall
483
CTOR;
484
    }
485
}
486