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