Completed
Push — 2.x ( 56f1be...dc7414 )
by Nikola
03:08
created

ClassProxy   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 487
Duplicated Lines 0 %

Coupling/Cohesion

Components 4
Dependencies 3

Test Coverage

Coverage 42.53%

Importance

Changes 0
Metric Value
wmc 58
lcom 4
cbo 3
dl 0
loc 487
ccs 74
cts 174
cp 0.4253
rs 4.8387
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
C __construct() 0 51 13
A setParentName() 0 6 1
A override() 0 6 1
A setMethod() 0 18 2
A injectJoinPoints() 0 15 2
B wrapWithJoinPoints() 0 30 6
A addInterface() 0 12 3
A addTrait() 0 12 3
A setProperty() 0 13 2
A addJoinpointsProperty() 0 14 1
A overrideMethod() 0 4 1
C getJoinpointInvocationBody() 0 24 7
A interceptProperty() 0 5 2
C __toString() 0 29 7
A addFieldInterceptorsCode() 0 10 2
B getConstructorBody() 0 27 5

How to fix   Complexity   

Complex Class

Complex classes like ClassProxy often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ClassProxy, and based on these observations, apply Extract Interface, too.

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