Completed
Branch feature/goaop-parser (08838c)
by Alexander
03:32
created

ClassProxy   D

Complexity

Total Complexity 60

Size/Duplication

Total Lines 556
Duplicated Lines 0 %

Coupling/Cohesion

Components 4
Dependencies 3

Test Coverage

Coverage 46.28%

Importance

Changes 18
Bugs 3 Features 2
Metric Value
c 18
b 3
f 2
dl 0
loc 556
wmc 60
lcom 4
cbo 3
ccs 87
cts 188
cp 0.4628
rs 4.2857

19 Methods

Rating   Name   Duplication   Size   Complexity  
C __construct() 0 50 13
A setParentName() 0 6 1
A override() 0 6 1
A setMethod() 0 18 2
A injectJoinPoints() 0 14 2
B wrapWithJoinPoints() 0 30 6
A addInterface() 0 12 3
A addTrait() 0 12 3
A setProperty() 0 13 2
A addJoinpointsProperty() 0 8 1
A overrideMethod() 0 7 3
A getJoinpointInvocationBody() 0 17 4
A interceptProperty() 0 5 2
C __toString() 0 31 7
A addFieldInterceptorsCode() 0 13 2
A getOverriddenMethod() 0 18 2
A getMagicGetterBody() 0 18 1
A getMagicSetterBody() 0 17 1
B getConstructorBody() 0 28 4

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