Completed
Pull Request — master (#395)
by Alexander
06:08
created

ClassProxyGenerator::getJoinpointInvocationBody()   B

Complexity

Conditions 6
Paths 24

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 6.0106

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 14
cts 15
cp 0.9333
rs 8.439
c 0
b 0
f 0
cc 6
eloc 15
nc 24
nop 1
crap 6.0106
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\Proxy;
22
use Go\Core\AspectContainer;
23
use Go\Core\AspectKernel;
24
use Go\Core\LazyAdvisorAccessor;
25
use Go\Proxy\Part\FunctionCallArgumentListGenerator;
26
use Go\Proxy\Part\InterceptedConstructorGenerator;
27
use Go\Proxy\Part\InterceptedMethodGenerator;
28
use Go\Proxy\Part\JoinPointPropertyGenerator;
29
use Go\Proxy\Part\PropertyInterceptionTrait;
30
use ReflectionClass;
31
use ReflectionMethod;
32
use Zend\Code\Generator\ClassGenerator;
33
use Zend\Code\Generator\DocBlockGenerator;
34
use Zend\Code\Reflection\DocBlockReflection;
35
36
/**
37
 * Class proxy builder that is used to generate a child class from the list of joinpoints
38
 */
39
class ClassProxyGenerator
40
{
41
42
    /**
43
     * Static mappings for class name for excluding if..else check
44
     */
45
    protected static $invocationClassMap = [
46
        AspectContainer::METHOD_PREFIX        => DynamicClosureMethodInvocation::class,
47
        AspectContainer::STATIC_METHOD_PREFIX => StaticClosureMethodInvocation::class,
48
        AspectContainer::PROPERTY_PREFIX      => ClassFieldAccess::class,
49
        AspectContainer::STATIC_INIT_PREFIX   => StaticInitializationJoinpoint::class,
50
        AspectContainer::INIT_PREFIX          => ReflectionConstructorInvocation::class
51
    ];
52
53
    /**
54
     * List of advices that are used for generation of child
55
     */
56
    protected $advices = [];
57
58
    /**
59
     * Instance of class generator
60
     */
61
    protected $generator;
62
63
    /**
64
     * Generates an child code by original class reflection and joinpoints for it
65
     *
66
     * @param ReflectionClass $originalClass        Original class reflection
67
     * @param string          $parentClassName      Parent class name to use
68
     * @param string[][]      $classAdvices         List of advices for class
69
     * @param bool            $useParameterWidening Enables usage of parameter widening feature
70
     */
71 8
    public function __construct(
72
        ReflectionClass $originalClass,
73
        string $parentClassName,
74
        array $classAdvices,
75
        bool $useParameterWidening
76
    ) {
77 8
        $this->advices         = $classAdvices;
78 8
        $dynamicMethodAdvices  = $classAdvices[AspectContainer::METHOD_PREFIX] ?? [];
79 8
        $staticMethodAdvices   = $classAdvices[AspectContainer::STATIC_METHOD_PREFIX] ?? [];
80 8
        $propertyAdvices       = $classAdvices[AspectContainer::PROPERTY_PREFIX] ?? [];
81 8
        $interceptedMethods    = array_keys($dynamicMethodAdvices + $staticMethodAdvices);
82 8
        $interceptedProperties = array_keys($propertyAdvices);
83 8
        $introducedInterfaces  = $classAdvices[AspectContainer::INTRODUCTION_INTERFACE_PREFIX] ?? [];
84 8
        $introducedTraits      = $classAdvices[AspectContainer::INTRODUCTION_TRAIT_PREFIX] ?? [];
85
86 8
        $generatedProperties = [new JoinPointPropertyGenerator($classAdvices)];
87 8
        $generatedMethods    = $this->interceptMethods($originalClass, $interceptedMethods);
88
89 8
        $introducedInterfaces[] = '\\' . Proxy::class;
90
91 8
        if (!empty($interceptedProperties)) {
92
            $generatedMethods['__construct'] = new InterceptedConstructorGenerator(
93
                $interceptedProperties,
94
                $originalClass->getConstructor(),
95
                $generatedMethods['__construct'] ?? null,
96
                $useParameterWidening
97
            );
98
            $introducedTraits[] = '\\' . PropertyInterceptionTrait::class;
99
        }
100
101 8
        $this->generator = new ClassGenerator(
102 8
            $originalClass->getShortName(),
103 8
            $originalClass->getNamespaceName(),
104 8
            $originalClass->isFinal() ? ClassGenerator::FLAG_FINAL : null,
105 8
            $parentClassName,
106 8
            $introducedInterfaces,
107 8
            $generatedProperties,
108 8
            $generatedMethods
109
        );
110 8
        if ($originalClass->getDocComment()) {
111
            $reflectionDocBlock = new DocBlockReflection($originalClass->getDocComment());
112
            $this->generator->setDocBlock(DocBlockGenerator::fromReflection($reflectionDocBlock));
113
        }
114
115 8
        $this->generator->addTraits($introducedTraits);
116 8
    }
117
118
    /**
119
     * Adds use alias for this class
120
     */
121
    public function addUse(string $use, string $useAlias = null): void
122
    {
123
        $this->generator->addUse($use, $useAlias);
124
    }
125
126
    /**
127
     * Inject advices into given class
128
     *
129
     * NB This method will be used as a callback during source code evaluation to inject joinpoints
130
     */
131
    public static function injectJoinPoints(string $targetClassName): void
132
    {
133
        $reflectionClass    = new ReflectionClass($targetClassName);
134
        $joinPointsProperty = $reflectionClass->getProperty(JoinPointPropertyGenerator::NAME);
135
136
        $joinPointsProperty->setAccessible(true);
137
        $advices    = $joinPointsProperty->getValue();
138
        $joinPoints = static::wrapWithJoinPoints($advices, $reflectionClass->getParentClass()->name);
139
        $joinPointsProperty->setValue($joinPoints);
140
141
        $staticInit = AspectContainer::STATIC_INIT_PREFIX . ':root';
142
        if (isset($joinPoints[$staticInit])) {
143
            $joinPoints[$staticInit]->__invoke();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Go\Aop\Intercept\Joinpoint as the method __invoke() does only exist in the following implementations of said interface: Go\Aop\Framework\AbstractMethodInvocation, Go\Aop\Framework\ClassFieldAccess, Go\Aop\Framework\DynamicClosureMethodInvocation, Go\Aop\Framework\ReflectionConstructorInvocation, Go\Aop\Framework\ReflectionFunctionInvocation, Go\Aop\Framework\StaticClosureMethodInvocation, Go\Aop\Framework\StaticInitializationJoinpoint.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
144
        }
145
    }
146
147
    /**
148
     * Generates the source code of child class
149
     */
150 8
    public function generate(): string
151
    {
152 8
        $classCode = $this->generator->generate();
153
154
        return $classCode
155
            // Inject advices on call
156 8
            . '\\' . __CLASS__ . '::injectJoinPoints(' . $this->generator->getName() . '::class);';
157
    }
158
159
    /**
160
     * Wrap advices with joinpoint object
161
     *
162
     * @param array|Advice[][][] $classAdvices Advices for specific class
163
     *
164
     * @throws \UnexpectedValueException If joinPoint type is unknown
165
     *
166
     * NB: Extension should be responsible for wrapping advice with join point.
167
     *
168
     * @return Joinpoint[] returns list of joinpoint ready to use
169
     */
170
    protected static function wrapWithJoinPoints(array $classAdvices, string $className): array
171
    {
172
        /** @var LazyAdvisorAccessor $accessor */
173
        static $accessor;
174
175
        if (!isset($accessor)) {
176
            $aspectKernel = AspectKernel::getInstance();
177
            $accessor     = $aspectKernel->getContainer()->get('aspect.advisor.accessor');
178
        }
179
180
        $joinPoints = [];
181
182
        foreach ($classAdvices as $joinPointType => $typedAdvices) {
183
            // if not isset then we don't want to create such invocation for class
184
            if (!isset(self::$invocationClassMap[$joinPointType])) {
185
                continue;
186
            }
187
            foreach ($typedAdvices as $joinPointName => $advices) {
188
                $filledAdvices = [];
189
                foreach ($advices as $advisorName) {
190
                    $filledAdvices[] = $accessor->$advisorName;
191
                }
192
193
                $joinpoint = new self::$invocationClassMap[$joinPointType]($className, $joinPointName, $filledAdvices);
194
                $joinPoints["$joinPointType:$joinPointName"] = $joinpoint;
195
            }
196
        }
197
198
        return $joinPoints;
199
    }
200
201
    /**
202
     * Returns list of intercepted method generators for class by method names
203
     *
204
     * @param string[] $methodNames List of methods to intercept
205
     *
206
     * @return InterceptedMethodGenerator[]
207
     */
208 8
    protected function interceptMethods(ReflectionClass $originalClass, array $methodNames): array
209
    {
210 8
        $interceptedMethods = [];
211 8
        foreach ($methodNames as $methodName) {
212 8
            $reflectionMethod = $originalClass->getMethod($methodName);
213 8
            $methodBody       = $this->getJoinpointInvocationBody($reflectionMethod);
214
215 8
            $interceptedMethods[$methodName] = new InterceptedMethodGenerator($reflectionMethod, $methodBody);
216
        }
217
218 8
        return $interceptedMethods;
219
    }
220
221
    /**
222
     * Creates string definition for method body by method reflection
223
     */
224 8
    protected function getJoinpointInvocationBody(ReflectionMethod $method): string
225
    {
226 8
        $isStatic = $method->isStatic();
227 8
        $scope    = $isStatic ? 'static::class' : '$this';
228 8
        $prefix   = $isStatic ? AspectContainer::STATIC_METHOD_PREFIX : AspectContainer::METHOD_PREFIX;
229
230 8
        $argumentList = new FunctionCallArgumentListGenerator($method);
231 8
        $argumentCode = $argumentList->generate();
232 8
        $return = 'return ';
233 8
        if ($method->hasReturnType()) {
234 1
            $returnType = (string) $method->getReturnType();
235 1
            if ($returnType === 'void') {
236
                // void return types should not return anything
237
                $return = '';
238
            }
239
        }
240
241 8
        if (!empty($argumentCode)) {
242 4
            $scope = "$scope, $argumentCode";
243
        }
244
245 8
        $body = "{$return}self::\$__joinPoints['{$prefix}:{$method->name}']->__invoke($scope);";
246
247 8
        return $body;
248
    }
249
}
250