Completed
Push — master ( 509a2e...170574 )
by Alexander
07:07
created

ClassProxy::setMappings()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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