Completed
Pull Request — master (#40)
by Bernhard
04:17
created

AdviceFilter::filter()   C

Complexity

Conditions 8
Paths 6

Size

Total Lines 62
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 8
Bugs 0 Features 4
Metric Value
c 8
b 0
f 4
dl 0
loc 62
rs 6.943
cc 8
eloc 26
nc 6
nop 4

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * \AppserverIo\Doppelgaenger\StreamFilters\AdviceFilter
5
 *
6
 * NOTICE OF LICENSE
7
 *
8
 * This source file is subject to the Open Software License (OSL 3.0)
9
 * that is available through the world-wide-web at this URL:
10
 * http://opensource.org/licenses/osl-3.0.php
11
 *
12
 * PHP version 5
13
 *
14
 * @author    Bernhard Wick <[email protected]>
15
 * @copyright 2015 TechDivision GmbH - <[email protected]>
16
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
17
 * @link      https://github.com/appserver-io/doppelgaenger
18
 * @link      http://www.appserver.io/
19
 */
20
21
namespace AppserverIo\Doppelgaenger\StreamFilters;
22
23
use AppserverIo\Doppelgaenger\AspectRegister;
24
use AppserverIo\Doppelgaenger\Dictionaries\ReservedKeywords;
25
use AppserverIo\Doppelgaenger\Entities\Definitions\FunctionDefinition;
26
use AppserverIo\Doppelgaenger\Dictionaries\Placeholders;
27
use AppserverIo\Doppelgaenger\Entities\Joinpoint;
28
use AppserverIo\Doppelgaenger\Entities\Lists\FunctionDefinitionList;
29
use AppserverIo\Doppelgaenger\Entities\Lists\PointcutExpressionList;
30
use AppserverIo\Doppelgaenger\Entities\Pointcuts\AdvisePointcut;
31
use AppserverIo\Doppelgaenger\Entities\Pointcuts\AndPointcut;
32
use AppserverIo\Doppelgaenger\Entities\Pointcuts\PointcutPointcut;
33
use AppserverIo\Psr\MetaobjectProtocol\Aop\Annotations\Advices\Around;
34
use AppserverIo\Psr\MetaobjectProtocol\Aop\Annotations\Advices\Before;
35
36
/**
37
 * This filter will buffer the input stream and add all advice calls into their respective join-point locations
38
 *
39
 * @author    Bernhard Wick <[email protected]>
40
 * @copyright 2015 TechDivision GmbH - <[email protected]>
41
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
42
 * @link      https://github.com/appserver-io/doppelgaenger
43
 * @link      http://www.appserver.io/
44
 */
45
class AdviceFilter extends AbstractFilter
46
{
47
    /**
48
     * Order number if filters are used as a stack, higher means below others
49
     *
50
     * @const integer FILTER_ORDER
51
     */
52
    const FILTER_ORDER = 2;
53
54
    /**
55
     * @var  $aspectRegister
56
     */
57
    protected $aspectRegister;
58
59
    /**
60
     * Other filters on which we depend
61
     *
62
     * @var array $dependencies
63
     */
64
    protected $dependencies = array('SkeletonFilter');
65
66
    /**
67
     * Filter a chunk of data by adding postcondition checks
68
     *
69
     * @param string                 $chunk               The data chunk to be filtered
70
     * @param FunctionDefinitionList $functionDefinitions Definition of the structure the chunk belongs to
71
     * @param AspectRegister         $aspectRegister      The register containing potential aspects
72
     *
73
     * @return string
74
     */
75
    public function filterChunk($chunk, FunctionDefinitionList $functionDefinitions, AspectRegister $aspectRegister)
76
    {
77
        $this->aspectRegister = $aspectRegister;
78
79
        // get the tokens
80
        $tokens = token_get_all($chunk);
81
82
        // go through the tokens and check what we found
83
        $tokensCount = count($tokens);
84
        for ($i = 0; $i < $tokensCount; $i++) {
85
            // did we find a function? If so check if we know that thing and insert the code of its preconditions.
86
            if (is_array($tokens[$i]) && $tokens[$i][0] === T_FUNCTION && is_array($tokens[$i + 2])) {
87
                // get the name of the function
88
                $functionName = $tokens[$i + 2][1];
89
90
                // check if we got the function in our list, if not continue
91
                $functionDefinition = $functionDefinitions->get($functionName);
92
93
                if (!$functionDefinition instanceof FunctionDefinition) {
94
                    continue;
95
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
96
                } else {
97
                    // collect all pointcut expressions, advice based as well as directly defined ones
98
                    $pointcutExpressions = $functionDefinition->getPointcutExpressions();
99
                    $pointcutExpressions->attach($this->findAdvicePointcutExpressions($functionDefinition));
100
101
                    if ($pointcutExpressions->count() > 0) {
102
                        // sort all relevant pointcut expressions by their joinpoint code hooks
103
                        $sortedFunctionPointcuts = $this->sortPointcutExpressions($pointcutExpressions);
104
105
                        // get all the callbacks for around advices to build a proper advice chain
106
                        $callbackChain = $this->generateAdviceCallbacks($sortedFunctionPointcuts, $functionDefinition);
107
108
                        // before we weave in any advice code we have to make a MethodInvocation object ready
109
                        $this->injectInvocationCode($chunk, $functionDefinition, $callbackChain);
110
111
                        // as we need the result of the method invocation we have to collect it
112
                        $this->injectResultInjection($chunk, $functionName);
113
114
                        // we need the exception (if any) within our method invocation object
115
                        $this->injectExceptionInjection($chunk, $functionName);
116
117
                        // inject the advice code
118
                        $this->injectAdviceCode($chunk, $sortedFunctionPointcuts, $functionDefinition);
119
                    }
120
121
                    // "destroy" function definition
122
                    $functionDefinition = null;
0 ignored issues
show
Unused Code introduced by
$functionDefinition is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
123
                }
124
            }
125
        }
126
127
        return $chunk;
128
    }
129
130
    /**
131
     * Will inject the advice code for the different join-points based on sorted join-points
132
     *
133
     * @param string                                                             $bucketData                Reference on the current bucket's data
134
     * @param array                                                              $sortedPointcutExpressions Array of pointcut expressions sorted by join-points
135
     * @param \AppserverIo\Doppelgaenger\Entities\Definitions\FunctionDefinition $functionDefinition        Name of the function to inject the advices into
136
     *
137
     * @return boolean
138
     */
139
    protected function injectAdviceCode(& $bucketData, array $sortedPointcutExpressions, $functionDefinition)
140
    {
141
        // iterate over the sorted pointcuts and insert the code
142
        $functionName = $functionDefinition->getName();
143
        foreach ($sortedPointcutExpressions as $joinpoint => $pointcutExpressions) {
144
            // only do something if we got expressions
145
            if (empty($pointcutExpressions)) {
146
                continue;
147
            }
148
149
            // get placeholder and replacement prefix based on join-point
150
            $placeholderName = strtoupper($joinpoint) . '_JOINPOINT';
151
            $placeholderHook = constant('\AppserverIo\Doppelgaenger\Dictionaries\Placeholders::' . $placeholderName) .
152
                $functionName . Placeholders::PLACEHOLDER_CLOSE;
153
154
            // around advices have to be woven differently
155
            if ($joinpoint === Around::ANNOTATION && !$pointcutExpressions[0]->getPointcut() instanceof PointcutPointcut) {
156
                // insert the code but make sure to inject only the first one in the row, as the advice chain will
157
                // be implemented via the advice chain
158
                $pointcutExpression = $pointcutExpressions[0];
159
                $bucketData = str_replace(
160
                    $placeholderHook,
161
                    $pointcutExpression->toCode(),
162
                    $bucketData
163
                );
164
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
165
            } else {
166
                // iterate all the others and inject the code
167
                $parameterAssignmentCode = '';
168
                foreach ($pointcutExpressions as $pointcutExpression) {
169
                    if ($joinpoint === Before::ANNOTATION) {
170
                        // before advices have to be woven differently as we have to take changes of the call parameters into account
171
172
                        // we have to build up an assignment of the potentially altered parameters in our method invocation object
173
                        // to the original parameters of the method call.
174
                        // This way we can avoid e.g. broken references by func_get_args and other problems
175
                        $parameterNames = array();
176
                        foreach ($functionDefinition->getParameterDefinitions() as $parameterDefinition) {
177
                            $parameterNames[] = $parameterDefinition->name;
178
                        }
179
                        // only expose parameter values if there are any as of PHP 7.0 empty list() calls are forbidden
180
                        // @see http://php.net/manual/en/migration70.incompatible.php#migration70.incompatible.variable-handling.list.empty
181
                        if (count($parameterNames) > 0) {
182
                            $parameterAssignmentCode = 'list(' .
183
                                implode(',', $parameterNames) .
184
                                ') = array_values(' . ReservedKeywords::METHOD_INVOCATION_OBJECT . '->getParameters());';
185
                        }
186
                        // insert the actual before code
187
                        $bucketData = str_replace(
188
                            $placeholderHook,
189
                            $pointcutExpression->toCode() . $placeholderHook,
190
                            $bucketData
191
                        );
192
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
193
                    } else {
194
                        // all join-point NOT Before or Around can be woven very simply
195
                        $bucketData = str_replace(
196
                            $placeholderHook,
197
                            $placeholderHook . $pointcutExpression->toCode(),
198
                            $bucketData
199
                        );
200
                    }
201
                }
202
203
                // if the before join-point has been used we also have a parameter assignment code we have to insert
204
                if (!empty($parameterAssignmentCode)) {
205
                    $bucketData = str_replace(
206
                        $placeholderHook,
207
                        $parameterAssignmentCode . $placeholderHook,
208
                        $bucketData
209
                    );
210
                }
211
            }
212
        }
213
214
        return true;
215
    }
216
217
    /**
218
     * Will inject the result of the original method invocation into our method invocation object as we need it for later use
219
     *
220
     * @param string $bucketData   Reference on the current bucket's data
221
     * @param string $functionName Name of the function to inject the advices into
222
     *
223
     * @return boolean
224
     */
225 View Code Duplication
    protected function injectResultInjection(& $bucketData, $functionName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
226
    {
227
        $placeholderHook = Placeholders::AROUND_JOINPOINT . $functionName . Placeholders::PLACEHOLDER_CLOSE;
228
        $bucketData = str_replace(
229
            $placeholderHook,
230
            $placeholderHook . '
231
            ' . ReservedKeywords::METHOD_INVOCATION_OBJECT . '->injectResult(' . ReservedKeywords::RESULT . ');',
232
            $bucketData
233
        );
234
    }
235
236
    /**
237
     * Will inject the injection of any thrown exception into our method invocation object as we need it for later use
238
     *
239
     * @param string $bucketData   Reference on the current bucket's data
240
     * @param string $functionName Name of the function to inject the advices into
241
     *
242
     * @return boolean
243
     */
244 View Code Duplication
    protected function injectExceptionInjection(& $bucketData, $functionName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
245
    {
246
        $placeholderHook = Placeholders::AFTERTHROWING_JOINPOINT . $functionName . Placeholders::PLACEHOLDER_CLOSE;
247
        $bucketData = str_replace(
248
            $placeholderHook,
249
            ReservedKeywords::METHOD_INVOCATION_OBJECT . '->injectThrownException(' . ReservedKeywords::THROWN_EXCEPTION_OBJECT . ');
250
            ' . $placeholderHook,
251
            $bucketData
252
        );
253
    }
254
255
    /**
256
     * Will look at all advices known to the aspect register and filter out pointcut expressions matching the
257
     * function in question
258
     * Will return a list of pointcut expressions including distinctive pointcuts to weave in the associated advices
259
     *
260
     * @param FunctionDefinition $functionDefinition Definition of the function to match known advices against
261
     *
262
     * @return \AppserverIo\Doppelgaenger\Entities\Lists\PointcutExpressionList
263
     */
264
    protected function findAdvicePointcutExpressions(FunctionDefinition $functionDefinition)
265
    {
266
        // we have to search for all advices, all of their pointcuts and all pointcut expressions those reference
267
        $pointcutExpressions = new PointcutExpressionList();
268
        foreach ($this->aspectRegister as $aspect) {
269
            foreach ($aspect->getAdvices() as $advice) {
270
                foreach ($advice->getPointcuts() as $pointcut) {
271
                    // there should be no other pointcuts than those referencing pointcut definitions
272
                    if (!$pointcut instanceof PointcutPointcut) {
273
                        continue;
274
                    }
275
276
                    foreach ($pointcut->getReferencedPointcuts() as $referencedPointcut) {
277
                        if ($referencedPointcut->getPointcutExpression()->getPointcut()->matches($functionDefinition)) {
0 ignored issues
show
Bug introduced by
The method getPointcutExpression() does not seem to exist on object<AppserverIo\Doppe...aces\PointcutInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
278
                            // we found a pointcut of an advice that matches!
279
                            // lets create a distinctive join-point and add the advice weaving to the pointcut.
280
                            // Make a clone so that there are no weird reference shenanigans
281
                            $pointcutExpression = clone $referencedPointcut->getPointcutExpression();
0 ignored issues
show
Bug introduced by
The method getPointcutExpression() does not seem to exist on object<AppserverIo\Doppe...aces\PointcutInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
282
                            $joinpoint = new Joinpoint();
283
                            $joinpoint->setCodeHook($advice->getCodeHook());
284
                            $joinpoint->setStructure($functionDefinition->getStructureName());
285
                            $joinpoint->setTarget(Joinpoint::TARGET_METHOD);
286
                            $joinpoint->setTargetName($functionDefinition->getName());
287
288
                            $pointcutExpression->setJoinpoint($joinpoint);
289
290
                            // "straighten out" structure and function referenced by the pointcut to avoid regex within generated code.
291
                            // Same here with the cloning: we do not want to influence the matching process with working on references
292
                            $expressionPointcut = clone $pointcutExpression->getPointcut();
293
                            $expressionPointcut->straightenExpression($functionDefinition);
294
295
                            // add the weaving pointcut into the expression
296
                            $pointcutExpression->setPointcut(new AndPointcut(
297
                                AdvisePointcut::TYPE . str_replace('\\\\', '\\', '(\\' . $advice->getQualifiedName() . ')'),
298
                                $expressionPointcut->getType() . '(' . $expressionPointcut->getExpression() . ')'
299
                            ));
300
                            $pointcutExpression->setString($expressionPointcut->getExpression());
301
302
                            // add it to our result list
303
                            $pointcutExpressions->add($pointcutExpression);
304
305
                            // break here as we only need one, they are implicitly "or" combined
306
                            break;
307
                        }
308
                    }
309
                }
310
            }
311
        }
312
313
        return $pointcutExpressions;
314
    }
315
316
    /**
317
     * Will inject invocation code for a given function into a given piece of code.
318
     * Invocation code will be the instantiation of a \AppserverIo\Doppelgaenger\Entities\MethodInvocation object
319
     * as a basic representation of the given function
320
     *
321
     * @param string             $bucketData         Reference on the current bucket's data
322
     * @param FunctionDefinition $functionDefinition Definition of the function to inject invocation code into
323
     * @param array              $callbackChain      Chain of callbacks which is used to recursively chain calls
324
     *
325
     * @return boolean
326
     */
327
    protected function injectInvocationCode(& $bucketData, FunctionDefinition $functionDefinition, array $callbackChain)
328
    {
329
330
        // start building up the code
331
        $code = '
332
            ' . ReservedKeywords::METHOD_INVOCATION_OBJECT . ' = new \AppserverIo\Doppelgaenger\Entities\MethodInvocation(
333
            ';
334
335
        // add the original method call to the callback chain so it can be integrated, add it and add the context
336
        if ($functionDefinition->isStatic()) {
337
            $contextCode = '__CLASS__';
338
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
339
        } else {
340
            $contextCode = '$this';
341
        }
342
343
        // iterate the callback chain and build up the code but pop the first element as we will invoke it initially
344
        unset($callbackChain[0]);
345
346
        // empty chain? Add the original function at least
347
        if (empty($callbackChain)) {
348
            $callbackChain[] = array($functionDefinition->getStructureName(), $functionDefinition->getName());
349
        }
350
351
        $code .= '    array(';
352
        foreach ($callbackChain as $callback) {
353
            // do some brushing up for the structure
354
            $structure = $callback[0];
355
            if ($structure === $functionDefinition->getStructureName()) {
356
                $structure = $contextCode;
357
            }
358
359
            // also brush up the function call to direct to the original
360
            if ($callback[1] === $functionDefinition->getName()) {
361
                $callback[1] = $functionDefinition->getName() . ReservedKeywords::ORIGINAL_FUNCTION_SUFFIX;
362
            }
363
364
            $code .= 'array(' . $structure . ', \'' . $callback[1] . '\'),';
365
        }
366
        $code .= '),
367
        ';
368
369
        // continue with the access modifiers
370
        $code .= '        ' . $contextCode . ',
371
                ' . ($functionDefinition->isAbstract() ? 'true' : 'false') . ',
372
                ' . ($functionDefinition->isFinal() ? 'true' : 'false') . ',
373
                ' . ($functionDefinition->isStatic() ? 'true' : 'false') . ',
374
            ';
375
376
        // we have to build up manual parameter collection as func_get_args() only returns copies
377
        // @see http://php.net/manual/en/function.func-get-args.php
378
        $parametersCode = '    array(';
379
        foreach ($functionDefinition->getParameterDefinitions() as $parameterDefinition) {
380
            $name = $parameterDefinition->name;
381
            $parametersCode .= '\'' . substr($name, 1) . '\' => ' . $name . ',';
382
        }
383
        $parametersCode .= ')';
384
385
        $code .= '    \'' . $functionDefinition->getName() . '\',
386
            ' .$parametersCode . ',
387
                __CLASS__,
388
                \'' . $functionDefinition->getVisibility() . '\'
389
            );';
390
391
        // Insert the code
392
        $placeholder = Placeholders::FUNCTION_BEGIN . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE;
393
        $bucketData = str_replace(
394
            $placeholder,
395
            $placeholder . $code,
396
            $bucketData
397
        );
398
399
        return true;
400
    }
401
402
    /**
403
     * Will generate and advice chain of callbacks to the given around pointcut expressions
404
     *
405
     * @param array              $sortedPointcutExpressions Pointcut expressions sorted by their join-point's code hooks
406
     * @param FunctionDefinition $functionDefinition        Definition of the function to inject invocation code into
407
     *
408
     * @return array
409
     */
410
    protected function generateAdviceCallbacks(array $sortedPointcutExpressions, FunctionDefinition $functionDefinition)
411
    {
412
413
        // collect the callback chains of the involved pointcut expressions
414
        $callbackChain = array();
415
        if (isset($sortedPointcutExpressions[Around::ANNOTATION])) {
416
            foreach ($sortedPointcutExpressions[Around::ANNOTATION] as $aroundExpression) {
417
                $callbackChain = array_merge($callbackChain, $aroundExpression->getPointcut()->getCallbackChain($functionDefinition));
418
            }
419
        }
420
421
        // filter the combined callback chain to avoid doubled calls to the original implementation
422
        $callbackChainCount = (count($callbackChain) - 1);
423
        for ($i = 0; $i < $callbackChainCount; $i ++) {
424
            if ($callbackChain[$i][1] === $functionDefinition->getName()) {
425
                unset($callbackChain[$i]);
426
            }
427
        }
428
429
        return $callbackChain;
430
    }
431
432
    /**
433
     * Will sort a list of given pointcut expressions based on the joinpoints associated with them
434
     *
435
     * @param \AppserverIo\Doppelgaenger\Entities\Lists\PointcutExpressionList $pointcutExpressions List of pointcut
436
     *          expressions
437
     *
438
     * @return array
439
     */
440
    protected function sortPointcutExpressions($pointcutExpressions)
441
    {
442
        // sort by join-point code hooks
443
        $sortedPointcutExpressions = array();
444
        foreach ($pointcutExpressions as $pointcutExpression) {
445
            $sortedPointcutExpressions[$pointcutExpression->getJoinpoint()->getCodeHook()][] = $pointcutExpression;
446
        }
447
448
        return $sortedPointcutExpressions;
449
    }
450
}
451