Completed
Pull Request — master (#249)
by Alexander
03:08
created

ClassProxy::setMethod()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 18
ccs 0
cts 13
cp 0
rs 9.4285
cc 2
eloc 14
nc 2
nop 5
crap 6
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\MethodInvocationComposer;
17
use Go\Aop\Framework\ReflectionConstructorInvocation;
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 as Method;
27
use ReflectionProperty as Property;
28
use TokenReflection\ReflectionClass as ParsedClass;
29
use TokenReflection\ReflectionMethod as ParsedMethod;
30
use TokenReflection\ReflectionProperty as ParsedProperty;
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|ParsedClass
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 = null;
64
65
    /**
66
     * List of additional interfaces to implement
67
     *
68
     * @var array
69
     */
70
    protected $interfaces = [];
71
72
    /**
73
     * List of additional traits for using
74
     *
75
     * @var array
76
     */
77
    protected $traits = [];
78
79
    /**
80
     * Source code for properties
81
     *
82
     * @var array Name of property => source code for it
83
     */
84
    protected $propertiesCode = [];
85
86
    /**
87
     * Name for the current class
88
     *
89
     * @var string
90
     */
91
    protected $name = '';
92
93
    /**
94
     * Flag to determine if we need to add a code for property interceptors
95
     *
96
     * @var bool
97
     */
98
    private $isFieldsIntercepted = false;
99
100
    /**
101
     * List of intercepted properties names
102
     *
103
     * @var array
104
     */
105
    private $interceptedProperties = [];
106
107
    /**
108
     * Generates an child code by parent class reflection and joinpoints for it
109
     *
110
     * @param ParsedClass $parent Parent class reflection
111
     * @param array|Advice[] $classAdvices List of advices for class
112
     *
113
     * @throws \InvalidArgumentException if there are unknown type of advices
114
     */
115 5
    public function __construct(ParsedClass $parent, array $classAdvices)
116
    {
117 5
        parent::__construct($classAdvices);
118
119 5
        $this->class           = $parent;
120 5
        $this->name            = $parent->getShortName();
121 5
        $this->parentClassName = $parent->getShortName();
122
123 5
        $this->addInterface('\Go\Aop\Proxy');
124 5
        $this->addJoinpointsProperty();
125
126 5
        foreach ($classAdvices as $type => $typedAdvices) {
127
128
            switch ($type) {
129 5
                case AspectContainer::METHOD_PREFIX:
130
                case AspectContainer::STATIC_METHOD_PREFIX:
131 5
                    foreach ($typedAdvices as $joinPointName => $advice) {
132 5
                        $method = $parent->getMethod($joinPointName);
133 5
                        if (!$method instanceof ParsedMethod) {
0 ignored issues
show
Bug introduced by
The class TokenReflection\ReflectionMethod does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
134
                            continue;
135
                        }
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
                        if (!$property instanceof ParsedProperty) {
0 ignored issues
show
Bug introduced by
The class TokenReflection\ReflectionProperty does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
144
                            continue;
145
                        }
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 5
                    throw new \InvalidArgumentException("Unsupported point `$type`");
168
            }
169
        }
170 5
    }
171
172
173
    /**
174
     * Updates parent name for child
175
     *
176
     * @param string $newParentName New class name
177
     *
178
     * @return static
179
     */
180 5
    public function setParentName($newParentName)
181
    {
182 5
        $this->parentClassName = $newParentName;
183
184 5
        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 5
    public function override($methodName, $body)
196
    {
197 5
        $this->methodsCode[$methodName] = $this->getOverriddenMethod($this->class->getMethod($methodName), $body);
198
199 5
        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
     * Initialize static mappings to reduce the time for checking features
259
     *
260
     * @param bool $useSplatOperator Enables usage of optimized invocation with splat operator
261
     */
262
    protected static function setMappings($useSplatOperator)
263
    {
264
        $dynamicMethodClass = MethodInvocationComposer::compose(false, $useSplatOperator, false);
265
        $staticMethodClass  = MethodInvocationComposer::compose(true, $useSplatOperator, false);
266
267
        // We are using LSB here and overridden static property
268
        static::$invocationClassMap = array(
269
            AspectContainer::METHOD_PREFIX        => $dynamicMethodClass,
270
            AspectContainer::STATIC_METHOD_PREFIX => $staticMethodClass,
271
            AspectContainer::PROPERTY_PREFIX      => ClassFieldAccess::class,
272
            AspectContainer::STATIC_INIT_PREFIX   => StaticInitializationJoinpoint::class,
273
            AspectContainer::INIT_PREFIX          => ReflectionConstructorInvocation::class
274
        );
275
    }
276
277
    /**
278
     * Wrap advices with joinpoint object
279
     *
280
     * @param array|Advice[] $classAdvices Advices for specific class
281
     * @param string $className Name of the original class to use
282
     *
283
     * @throws \UnexpectedValueException If joinPoint type is unknown
284
     *
285
     * NB: Extension should be responsible for wrapping advice with join point.
286
     *
287
     * @return array|Joinpoint[] returns list of joinpoint ready to use
288
     */
289
    protected static function wrapWithJoinPoints($classAdvices, $className)
290
    {
291
        /** @var LazyAdvisorAccessor $accessor */
292
        static $accessor = null;
293
294
        if (!self::$invocationClassMap) {
295
            $aspectKernel = AspectKernel::getInstance();
296
            $accessor     = $aspectKernel->getContainer()->get('aspect.advisor.accessor');
297
            self::setMappings(
298
                $aspectKernel->hasFeature(Features::USE_SPLAT_OPERATOR)
299
            );
300
        }
301
302
        $joinPoints = [];
303
304
        foreach ($classAdvices as $joinPointType => $typedAdvices) {
305
            // if not isset then we don't want to create such invocation for class
306
            if (!isset(self::$invocationClassMap[$joinPointType])) {
307
                continue;
308
            }
309
            foreach ($typedAdvices as $joinPointName => $advices) {
310
                $filledAdvices = [];
311
                foreach ($advices as $advisorName) {
312
                    $filledAdvices[] = $accessor->$advisorName;
313
                }
314
315
                $joinpoint = new self::$invocationClassMap[$joinPointType]($className, $joinPointName, $filledAdvices);
316
                $joinPoints["$joinPointType:$joinPointName"] = $joinpoint;
317
            }
318
        }
319
320
        return $joinPoints;
321
    }
322
323
    /**
324
     * Add an interface for child
325
     *
326
     * @param string|ReflectionClass|ParsedClass $interface
327
     *
328
     * @throws \InvalidArgumentException If object is not an interface
329
     */
330 5
    public function addInterface($interface)
331
    {
332 5
        $interfaceName = $interface;
333 5
        if ($interface instanceof ReflectionClass || $interface instanceof ParsedClass) {
0 ignored issues
show
Bug introduced by
The class TokenReflection\ReflectionClass does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
334
            if (!$interface->isInterface()) {
335
                throw new \InvalidArgumentException("Interface expected to add");
336
            }
337
            $interfaceName = $interface->name;
338
        }
339
        // Use absolute namespace to prevent NS-conflicts
340 5
        $this->interfaces[] = '\\' . ltrim($interfaceName, '\\');
341 5
    }
342
343
    /**
344
     * Add a trait for child
345
     *
346
     * @param string|ReflectionClass|ParsedClass $trait
347
     *
348
     * @throws \InvalidArgumentException If object is not a trait
349
     */
350
    public function addTrait($trait)
351
    {
352
        $traitName = $trait;
353
        if ($trait instanceof ReflectionClass || $trait instanceof ParsedClass) {
0 ignored issues
show
Bug introduced by
The class TokenReflection\ReflectionClass does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
354
            if (!$trait->isTrait()) {
355
                throw new \InvalidArgumentException("Trait expected to add");
356
            }
357
            $traitName = $trait->name;
358
        }
359
        // Use absolute namespace to prevent NS-conflicts
360
        $this->traits[] = '\\' . ltrim($traitName, '\\');
361
    }
362
363
    /**
364
     * Creates a property
365
     *
366
     * @param int $propFlags See ReflectionProperty modifiers
367
     * @param string $propName Name of the property
368
     * @param null|string $defaultText Default value, should be string text!
369
     *
370
     * @return static
371
     */
372 5
    public function setProperty($propFlags, $propName, $defaultText = null)
373
    {
374 5
        $this->propertiesCode[$propName] = (
375
            "/**\n * Property was created automatically, do not change it manually\n */\n" . // Doc-block
376 5
            join(' ', Reflection::getModifierNames($propFlags)) . // List of modifiers for property
377 5
            ' $' . // Space and vaiable symbol
378 5
            $propName . // Name of the property
379 5
            (is_string($defaultText) ? " = $defaultText" : '') . // Default value if present
380 5
            ";\n" // End of line with property definition
381
        );
382
383 5
        return $this;
384
    }
385
386
    /**
387
     * Adds a definition for joinpoints private property in the class
388
     *
389
     * @return void
390
     */
391 5
    protected function addJoinpointsProperty()
392
    {
393 5
        $this->setProperty(
394 5
            Property::IS_PRIVATE | Property::IS_STATIC,
395 5
            '__joinPoints',
396 5
            '[]'
397
        );
398 5
    }
399
400
    /**
401
     * Override parent method with joinpoint invocation
402
     *
403
     * @param ParsedMethod $method Method reflection
404
     */
405 5
    protected function overrideMethod(ParsedMethod $method)
406
    {
407
        // temporary disable override of final methods
408 5
        if (!$method->isFinal() && !$method->isAbstract()) {
409 5
            $this->override($method->name, $this->getJoinpointInvocationBody($method));
410
        }
411 5
    }
412
413
    /**
414
     * Creates definition for method body
415
     *
416
     * @param ParsedMethod $method Method reflection
417
     *
418
     * @return string new method body
419
     */
420 5
    protected function getJoinpointInvocationBody(ParsedMethod $method)
421
    {
422 5
        $isStatic = $method->isStatic();
423 5
        $scope    = $isStatic ? self::$staticLsbExpression : '$this';
424 5
        $prefix   = $isStatic ? AspectContainer::STATIC_METHOD_PREFIX : AspectContainer::METHOD_PREFIX;
425
426 5
        $args = $this->prepareArgsLine($method);
427 5
        $body = '';
428
429 5
        if (!empty($args)) {
430 2
            $scope = "$scope, [$args]";
431
        }
432
433 5
        $body .= "return self::\$__joinPoints['{$prefix}:{$method->name}']->__invoke($scope);";
434
435 5
        return $body;
436
    }
437
438
    /**
439
     * Makes property intercepted
440
     *
441
     * @param ParsedProperty $property Reflection of property to intercept
442
     */
443
    protected function interceptProperty(ParsedProperty $property)
444
    {
445
        $this->interceptedProperties[] = is_object($property) ? $property->name : $property;
446
        $this->isFieldsIntercepted = true;
447
    }
448
449
    /**
450
     * {@inheritDoc}
451
     */
452 5
    public function __toString()
453
    {
454 5
        $ctor = $this->class->getConstructor();
455 5
        if ($this->isFieldsIntercepted && (!$ctor || !$ctor->isPrivate())) {
456
            $this->addFieldInterceptorsCode($ctor);
457
        }
458
459 5
        $prefix = join(' ', Reflection::getModifierNames($this->class->getModifiers()));
460
461
        $classCode = (
462 5
            $this->class->getDocComment() . "\n" . // Original doc-block
463 5
            ($prefix ? "$prefix " : '') . // List of class modifiers
464 5
            'class ' . // 'class' keyword with one space
465 5
            $this->name . // Name of the class
466 5
            ' extends ' . // 'extends' keyword with
467 5
            $this->parentClassName . // Name of the parent class
468 5
            ($this->interfaces ? ' implements ' . join(', ', $this->interfaces) : '') . "\n" . // Interfaces list
469 5
            "{\n" . // Start of class definition
470 5
            ($this->traits ? $this->indent('use ' . join(', ', $this->traits) . ';' . "\n") : '') . "\n" . // Traits list
471 5
            $this->indent(join("\n", $this->propertiesCode)) . "\n" . // Property definitions
472 5
            $this->indent(join("\n", $this->methodsCode)) . "\n" . // Method definitions
473 5
            "}" // End of class definition
474
        );
475
476
        return $classCode
477
            // Inject advices on call
478 5
            . PHP_EOL
479 5
            . '\\' . __CLASS__ . "::injectJoinPoints('"
480 5
                . $this->class->name . "',"
481 5
                . var_export($this->advices, true) . ");";
482
    }
483
484
    /**
485
     * Add code for intercepting properties
486
     *
487
     * @param null|ParsedMethod $constructor Constructor reflection or null
488
     */
489
    protected function addFieldInterceptorsCode(ParsedMethod $constructor = null)
490
    {
491
        $byReference = false;
492
        $this->setProperty(Property::IS_PRIVATE, '__properties', 'array()');
493
        $this->setMethod(Method::IS_PUBLIC, '__get', $byReference, $this->getMagicGetterBody(), '$name');
494
        $this->setMethod(Method::IS_PUBLIC, '__set', $byReference, $this->getMagicSetterBody(), '$name, $value');
495
        $this->isFieldsIntercepted = true;
496
        if ($constructor) {
497
            $this->override('__construct', $this->getConstructorBody($constructor, true));
498
        } else {
499
            $this->setMethod(Method::IS_PUBLIC, '__construct', $byReference, $this->getConstructorBody(), '');
500
        }
501
    }
502
503
    /**
504
     * Creates a method code from Reflection
505
     *
506
     * @param ParsedMethod $method Reflection for method
507
     * @param string $body Body of method
508
     *
509
     * @return string
510
     */
511 5
    protected function getOverriddenMethod(ParsedMethod $method, $body)
512
    {
513
        $code = (
514 5
            preg_replace('/ {4}|\t/', '', $method->getDocComment()) . "\n" . // Original Doc-block
515 5
            join(' ', Reflection::getModifierNames($method->getModifiers())) . // List of modifiers
516 5
            ' function ' . // 'function' keyword
517 5
            ($method->returnsReference() ? '&' : '') . // By reference symbol
518 5
            $method->name . // Name of the method
519 5
            '(' . // Start of parameters list
520 5
            join(', ', $this->getParameters($method->getParameters())) . // List of parameters
521 5
            ")\n" . // End of parameters list
522 5
            "{\n" . // Start of method body
523 5
            $this->indent($body) . "\n" . // Method body
524 5
            "}\n" // End of method body
525
        );
526
527 5
        return $code;
528
    }
529
530
    /**
531
     * Returns a code for magic getter to perform interception
532
     *
533
     * @return string
534
     */
535
    private function getMagicGetterBody()
536
    {
537
        return <<<'GETTER'
538
if (\array_key_exists($name, $this->__properties)) {
539
    return self::$__joinPoints["prop:$name"]->__invoke(
540
        $this,
541
        \Go\Aop\Intercept\FieldAccess::READ,
542
        $this->__properties[$name]
543
    );
544
} elseif (\method_exists(\get_parent_class(), __FUNCTION__)) {
545
    return parent::__get($name);
546
} else {
547
    trigger_error("Trying to access undeclared property {$name}");
548
549
    return null;
550
}
551
GETTER;
552
    }
553
554
    /**
555
     * Returns a code for magic setter to perform interception
556
     *
557
     * @return string
558
     */
559
    private function getMagicSetterBody()
560
    {
561
        return <<<'SETTER'
562
if (\array_key_exists($name, $this->__properties)) {
563
    $this->__properties[$name] = self::$__joinPoints["prop:$name"]->__invoke(
564
        $this,
565
        \Go\Aop\Intercept\FieldAccess::WRITE,
566
        $this->__properties[$name],
567
        $value
568
    );
569
} elseif (\method_exists(\get_parent_class(), __FUNCTION__)) {
570
    parent::__set($name, $value);
571
} else {
572
    $this->$name = $value;
573
}
574
SETTER;
575
    }
576
577
    /**
578
     * Returns constructor code
579
     *
580
     * @param ParsedMethod $constructor Constructor reflection
581
     * @param bool $isCallParent Is there is a need to call parent code
582
     *
583
     * @return string
584
     */
585
    private function getConstructorBody(ParsedMethod $constructor = null, $isCallParent = false)
586
    {
587
        $assocProperties = [];
588
        $listProperties  = [];
589
        foreach ($this->interceptedProperties as $propertyName) {
590
            $assocProperties[] = "'$propertyName' => \$this->$propertyName";
591
            $listProperties[]  = "\$this->$propertyName";
592
        }
593
        $assocProperties = $this->indent(join(',' . PHP_EOL, $assocProperties));
594
        $listProperties  = $this->indent(join(',' . PHP_EOL, $listProperties));
595
        if ($constructor) {
596
            $parentCall = $this->getJoinpointInvocationBody($constructor);
597
        } elseif ($isCallParent) {
598
            $parentCall = '\call_user_func_array(["parent", __FUNCTION__], \func_get_args());';
599
        } else {
600
            $parentCall = '';
601
        }
602
603
        return <<<CTOR
604
\$this->__properties = array(
605
$assocProperties
606
);
607
unset(
608
$listProperties
609
);
610
$parentCall
611
CTOR;
612
    }
613
}
614