Completed
Push — master ( f22ae4...0f9bab )
by Alexander
01:55
created

ClassProxy   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 454
Duplicated Lines 0 %

Coupling/Cohesion

Components 4
Dependencies 4

Test Coverage

Coverage 43.9%

Importance

Changes 0
Metric Value
wmc 55
lcom 4
cbo 4
dl 0
loc 454
ccs 72
cts 164
cp 0.439
rs 6
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
C __construct() 0 52 13
A setParentName() 0 4 1
A override() 0 4 1
A setMethod() 0 16 2
A injectJoinPoints() 0 15 2
B wrapWithJoinPoints() 0 30 6
A addInterface() 0 5 1
A addTrait() 0 5 1
A setProperty() 0 11 2
A addJoinpointsProperty() 0 14 1
A overrideMethod() 0 7 3
C getJoinpointInvocationBody() 0 24 7
A interceptProperty() 0 5 2
C __toString() 0 29 7
A addFieldInterceptorsCode() 0 10 2
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
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) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
132
133
            switch ($type) {
134 6
                case AspectContainer::METHOD_PREFIX:
135
                case AspectContainer::STATIC_METHOD_PREFIX:
136 6
                    foreach ($typedAdvices as $joinPointName => $advice) {
137 6
                        $method = $parent->getMethod($joinPointName);
138 6
                        $this->overrideMethod($method);
139
                    }
140 6
                    break;
141
142
                case AspectContainer::PROPERTY_PREFIX:
143
                    foreach ($typedAdvices as $joinPointName => $advice) {
144
                        $property = $parent->getProperty($joinPointName);
145
                        $this->interceptProperty($property);
146
                    }
147
                    break;
148
149
                case AspectContainer::INTRODUCTION_TRAIT_PREFIX:
150
                    foreach ($typedAdvices as $advice) {
151
                        /** @var $advice IntroductionInfo */
152
                        $introducedTrait = $advice->getTrait();
153
                        if (!empty($introducedTrait)) {
154
                            $this->addTrait($introducedTrait);
155
                        }
156
                        $introducedInterface = $advice->getInterface();
157
                        if (!empty($introducedInterface)) {
158
                            $this->addInterface($introducedInterface);
159
                        }
160
                    }
161
                    break;
162
163
                case AspectContainer::INIT_PREFIX:
164
                case AspectContainer::STATIC_INIT_PREFIX:
165
                    break; // No changes for class
166
167
                default:
168 6
                    throw new \InvalidArgumentException("Unsupported point `$type`");
169
            }
170
        }
171 6
    }
172
173
174
    /**
175
     * Updates parent name for child
176
     *
177
     * @param string $newParentName New class name
178
     */
179 6
    public function setParentName(string $newParentName)
180
    {
181 6
        $this->parentClassName = $newParentName;
182 6
    }
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 6
    public function override(string $methodName, string $body)
191
    {
192 6
        $this->methodsCode[$methodName] = $this->getOverriddenFunction($this->class->getMethod($methodName), $body);
193 6
    }
194
195
    /**
196
     * Creates a method
197
     *
198
     * @param int $methodFlags See ReflectionMethod modifiers
199
     * @param string $methodName Name of the method
200
     * @param bool $byReference Is method should return value by reference
201
     * @param string $body Body of method
202
     * @param string $parameters Definition of parameters
203
     */
204
    public function setMethod(int $methodFlags, string $methodName, bool $byReference, string $body, string $parameters)
205
    {
206
        $this->methodsCode[$methodName] = (
207
            "/**\n * Method was created automatically, do not change it manually\n */\n" .
208
            implode(' ', Reflection::getModifierNames($methodFlags)) . // List of method modifiers
209
            ' function ' . // 'function' keyword
210
            ($byReference ? '&' : '') . // Return value by reference
211
            $methodName . // Method name
212
            '(' . // Start of parameter list
213
            $parameters . // List of parameters
214
            ")\n" . // End of parameter list
215
            "{\n" . // Start of method body
216
            $this->indent($body) . "\n" . // Method body
217
            "}\n" // End of method body
218
        );
219
    }
220
221
    /**
222
     * Inject advices into given class
223
     *
224
     * NB This method will be used as a callback during source code evaluation to inject joinpoints
225
     *
226
     * @param string $className Aop child proxy class
227
     */
228
    public static function injectJoinPoints(string $className)
229
    {
230
        $reflectionClass    = new ReflectionClass($className);
231
        $joinPointsProperty = $reflectionClass->getProperty('__joinPoints');
232
233
        $joinPointsProperty->setAccessible(true);
234
        $advices    = $joinPointsProperty->getValue();
235
        $joinPoints = static::wrapWithJoinPoints($advices, $reflectionClass->getParentClass()->name);
236
        $joinPointsProperty->setValue($joinPoints);
237
238
        $staticInit = AspectContainer::STATIC_INIT_PREFIX . ':root';
239
        if (isset($joinPoints[$staticInit])) {
240
            $joinPoints[$staticInit]->__invoke();
241
        }
242
    }
243
244
    /**
245
     * Wrap advices with joinpoint object
246
     *
247
     * @param array|Advice[] $classAdvices Advices for specific class
248
     * @param string $className Name of the original class to use
249
     *
250
     * @throws \UnexpectedValueException If joinPoint type is unknown
251
     *
252
     * NB: Extension should be responsible for wrapping advice with join point.
253
     *
254
     * @return array|Joinpoint[] returns list of joinpoint ready to use
255
     */
256
    protected static function wrapWithJoinPoints(array $classAdvices, string $className): array
257
    {
258
        /** @var LazyAdvisorAccessor $accessor */
259
        static $accessor;
260
261
        if (!isset($accessor)) {
262
            $aspectKernel = AspectKernel::getInstance();
263
            $accessor     = $aspectKernel->getContainer()->get('aspect.advisor.accessor');
264
        }
265
266
        $joinPoints = [];
267
268
        foreach ($classAdvices as $joinPointType => $typedAdvices) {
269
            // if not isset then we don't want to create such invocation for class
270
            if (!isset(self::$invocationClassMap[$joinPointType])) {
271
                continue;
272
            }
273
            foreach ($typedAdvices as $joinPointName => $advices) {
274
                $filledAdvices = [];
275
                foreach ($advices as $advisorName) {
276
                    $filledAdvices[] = $accessor->$advisorName;
277
                }
278
279
                $joinpoint = new self::$invocationClassMap[$joinPointType]($className, $joinPointName, $filledAdvices);
280
                $joinPoints["$joinPointType:$joinPointName"] = $joinpoint;
281
            }
282
        }
283
284
        return $joinPoints;
285
    }
286
287
    /**
288
     * Add an interface for child
289
     *
290
     * @param string $interfaceName Name of the interface to add
291
     */
292 6
    public function addInterface(string $interfaceName)
293
    {
294
        // Use absolute namespace to prevent NS-conflicts
295 6
        $this->interfaces[] = '\\' . ltrim($interfaceName, '\\');
296 6
    }
297
298
    /**
299
     * Add a trait for child
300
     *
301
     * @param string $traitName Name of the trait to add
302
     */
303
    public function addTrait(string $traitName)
304
    {
305
        // Use absolute namespace to prevent NS-conflicts
306
        $this->traits[] = '\\' . ltrim($traitName, '\\');
307
    }
308
309
    /**
310
     * Creates a property
311
     *
312
     * @param int $propFlags See ReflectionProperty modifiers
313
     * @param string $propName Name of the property
314
     * @param null|string $defaultText Default value, should be string text!
315
     */
316 6
    public function setProperty(int $propFlags, string $propName, string $defaultText = null)
317
    {
318 6
        $this->propertiesCode[$propName] = (
319
            "/**\n * Property was created automatically, do not change it manually\n */\n" . // Doc-block
320 6
            implode(' ', Reflection::getModifierNames($propFlags)) . // List of modifiers for property
321 6
            ' $' . // Space and variable symbol
322 6
            $propName . // Name of the property
323 6
            (isset($defaultText) ? " = $defaultText" : '') . // Default value if present
324 6
            ";\n" // End of line with property definition
325
        );
326 6
    }
327
328
    /**
329
     * Adds a definition for joinpoints private property in the class
330
     */
331 6
    protected function addJoinpointsProperty()
332
    {
333 6
        $exportedAdvices = strtr(json_encode($this->advices, JSON_PRETTY_PRINT), [
334 6
            '{' => '[',
335
            '}' => ']',
336
            '"' => '\'',
337
            ':' => ' =>'
338
        ]);
339 6
        $this->setProperty(
340 6
            ReflectionProperty::IS_PRIVATE | ReflectionProperty::IS_STATIC,
341 6
            '__joinPoints',
342
            $exportedAdvices
343
        );
344 6
    }
345
346
    /**
347
     * Override parent method with joinpoint invocation
348
     *
349
     * @param ReflectionMethod $method Method reflection
350
     */
351 6
    protected function overrideMethod(ReflectionMethod $method)
352
    {
353
        // temporary disable override of final methods
354 6
        if (!$method->isFinal() && !$method->isAbstract()) {
355 6
            $this->override($method->name, $this->getJoinpointInvocationBody($method));
356
        }
357 6
    }
358
359
    /**
360
     * Creates definition for method body
361
     *
362
     * @param ReflectionMethod $method Method reflection
363
     *
364
     * @return string new method body
365
     */
366 6
    protected function getJoinpointInvocationBody(ReflectionMethod $method): string
367
    {
368 6
        $isStatic = $method->isStatic();
369 6
        $scope    = $isStatic ? self::$staticLsbExpression : '$this';
370 6
        $prefix   = $isStatic ? AspectContainer::STATIC_METHOD_PREFIX : AspectContainer::METHOD_PREFIX;
371
372 6
        $args   = $this->prepareArgsLine($method);
373 6
        $return = 'return ';
374 6
        if (PHP_VERSION_ID >= 70100 && $method->hasReturnType()) {
1 ignored issue
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class ReflectionMethod as the method hasReturnType() does only exist in the following sub-classes of ReflectionMethod: Go\ParserReflection\ReflectionMethod. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
375
            $returnType = (string) $method->getReturnType();
376
            if ($returnType === 'void') {
377
                // void return types should not return anything
378
                $return = '';
379
            }
380
        }
381
382 6
        if (!empty($args)) {
383 3
            $scope = "$scope, $args";
384
        }
385
386 6
        $body = "{$return}self::\$__joinPoints['{$prefix}:{$method->name}']->__invoke($scope);";
387
388 6
        return $body;
389
    }
390
391
    /**
392
     * Makes property intercepted
393
     *
394
     * @param ReflectionProperty $property Reflection of property to intercept
395
     */
396
    protected function interceptProperty(ReflectionProperty $property)
397
    {
398
        $this->interceptedProperties[] = is_object($property) ? $property->name : $property;
399
        $this->isFieldsIntercepted = true;
400
    }
401
402
    /**
403
     * {@inheritDoc}
404
     */
405 6
    public function __toString()
406
    {
407 6
        $ctor = $this->class->getConstructor();
408 6
        if ($this->isFieldsIntercepted && (!$ctor || !$ctor->isPrivate())) {
409
            $this->addFieldInterceptorsCode($ctor);
410
        }
411
412 6
        $prefix = implode(' ', Reflection::getModifierNames($this->class->getModifiers()));
413
414
        $classCode = (
415 6
            $this->class->getDocComment() . "\n" . // Original doc-block
416 6
            ($prefix ? "$prefix " : '') . // List of class modifiers
417 6
            'class ' . // 'class' keyword with one space
418 6
            $this->name . // Name of the class
419 6
            ' extends ' . // 'extends' keyword with
420 6
            $this->parentClassName . // Name of the parent class
421 6
            ($this->interfaces ? ' implements ' . implode(', ', $this->interfaces) : '') . "\n" . // Interfaces list
422 6
            "{\n" . // Start of class definition
423 6
            ($this->traits ? $this->indent('use ' . implode(', ', $this->traits) . ';' . "\n") : '') . "\n" . // Traits list
424 6
            $this->indent(implode("\n", $this->propertiesCode)) . "\n" . // Property definitions
425 6
            $this->indent(implode("\n", $this->methodsCode)) . "\n" . // Method definitions
426 6
            "}" // End of class definition
427
        );
428
429
        return $classCode
430
            // Inject advices on call
431 6
            . PHP_EOL
432 6
            . '\\' . __CLASS__ . "::injectJoinPoints(" . $this->class->getShortName() . "::class);";
433
    }
434
435
    /**
436
     * Add code for intercepting properties
437
     *
438
     * @param null|ReflectionMethod $constructor Constructor reflection or null
439
     */
440
    protected function addFieldInterceptorsCode(ReflectionMethod $constructor = null)
441
    {
442
        $this->addTrait(PropertyInterceptionTrait::class);
443
        $this->isFieldsIntercepted = true;
444
        if ($constructor) {
445
            $this->override('__construct', $this->getConstructorBody($constructor, true));
446
        } else {
447
            $this->setMethod(ReflectionMethod::IS_PUBLIC, '__construct', false, $this->getConstructorBody(), '');
448
        }
449
    }
450
451
    /**
452
     * Returns constructor code
453
     *
454
     * @param ReflectionMethod $constructor Constructor reflection
455
     * @param bool $isCallParent Is there is a need to call parent code
456
     *
457
     * @return string
458
     */
459
    private function getConstructorBody(ReflectionMethod $constructor = null, bool $isCallParent = false): string
460
    {
461
        $assocProperties = [];
462
        $listProperties  = [];
463
        foreach ($this->interceptedProperties as $propertyName) {
464
            $assocProperties[] = "'$propertyName' => &\$this->$propertyName";
465
            $listProperties[]  = "\$this->$propertyName";
466
        }
467
        $assocProperties = $this->indent(implode(',' . PHP_EOL, $assocProperties));
468
        $listProperties  = $this->indent(implode(',' . PHP_EOL, $listProperties));
469
        if (isset($this->methodsCode['__construct'])) {
470
            $parentCall = $this->getJoinpointInvocationBody($constructor);
0 ignored issues
show
Bug introduced by
It seems like $constructor defined by parameter $constructor on line 459 can be null; however, Go\Proxy\ClassProxy::getJoinpointInvocationBody() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
471
        } elseif ($isCallParent) {
472
            $parentCall = '\call_user_func_array(["parent", __FUNCTION__], \func_get_args());';
473
        } else {
474
            $parentCall = '';
475
        }
476
477
        return <<<CTOR
478
\$this->__properties = array(
479
$assocProperties
480
);
481
unset(
482
$listProperties
483
);
484
$parentCall
485
CTOR;
486
    }
487
}
488