AdviceFilter   B
last analyzed

Complexity

Total Complexity 43

Size/Duplication

Total Lines 416
Duplicated Lines 4.81 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
wmc 43
lcom 1
cbo 6
dl 20
loc 416
rs 8.96
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
B filter() 0 62 8
B injectAdviceCode() 0 74 9
A injectResultInjection() 10 10 1
A injectExceptionInjection() 10 10 1
B findAdvicePointcutExpressions() 0 51 7
D injectInvocationCode() 0 74 10
A generateAdviceCallbacks() 0 21 5
A sortPointcutExpressions() 0 10 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like AdviceFilter 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 AdviceFilter, and based on these observations, apply Extract Interface, too.

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\Dictionaries\ReservedKeywords;
24
use AppserverIo\Doppelgaenger\Entities\Definitions\FunctionDefinition;
25
use AppserverIo\Doppelgaenger\Dictionaries\Placeholders;
26
use AppserverIo\Doppelgaenger\Entities\Joinpoint;
27
use AppserverIo\Doppelgaenger\Entities\Lists\PointcutExpressionList;
28
use AppserverIo\Doppelgaenger\Entities\Pointcuts\AdvisePointcut;
29
use AppserverIo\Doppelgaenger\Entities\Pointcuts\AndPointcut;
30
use AppserverIo\Doppelgaenger\Entities\Pointcuts\PointcutPointcut;
31
use AppserverIo\Psr\MetaobjectProtocol\Aop\Annotations\Advices\Around;
32
use AppserverIo\Psr\MetaobjectProtocol\Aop\Annotations\Advices\Before;
33
34
/**
35
 * This filter will buffer the input stream and add all advice calls into their respective join-point locations
36
 *
37
 * @author    Bernhard Wick <[email protected]>
38
 * @copyright 2015 TechDivision GmbH - <[email protected]>
39
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
40
 * @link      https://github.com/appserver-io/doppelgaenger
41
 * @link      http://www.appserver.io/
42
 */
43
class AdviceFilter extends AbstractFilter
44
{
45
    /**
46
     * Order number if filters are used as a stack, higher means below others
47
     *
48
     * @const integer FILTER_ORDER
49
     */
50
    const FILTER_ORDER = 2;
51
52
    /**
53
     * @var  $aspectRegister
54
     */
55
    protected $aspectRegister;
56
57
    /**
58
     * Other filters on which we depend
59
     *
60
     * @var array $dependencies
61
     */
62
    protected $dependencies = array('SkeletonFilter');
63
64
    /**
65
     * The main filter method.
66
     * Implemented according to \php_user_filter class. Will loop over all stream buckets, buffer them and perform
67
     * the needed actions.
68
     *
69
     * @param resource $in       Incoming bucket brigade we need to filter
70
     * @param resource $out      Outgoing bucket brigade with already filtered content
71
     * @param integer  $consumed The count of altered characters as buckets pass the filter
72
     * @param boolean  $closing  Is the stream about to close?
73
     *
74
     * @return integer
75
     *
76
     * @link http://www.php.net/manual/en/php-user-filter.filter.php
77
     */
78
    public function filter($in, $out, &$consumed, $closing)
79
    {
80
        // get our buckets from the stream
81
        while ($bucket = stream_bucket_make_writeable($in)) {
82
            // get the tokens
83
            $tokens = token_get_all($bucket->data);
84
85
            $functionDefinitions = $this->params['functionDefinitions'];
86
            $this->aspectRegister = $this->params['aspectRegister'];
87
88
            // go through the tokens and check what we found
89
            $tokensCount = count($tokens);
90
            for ($i = 0; $i < $tokensCount; $i++) {
91
                // did we find a function? If so check if we know that thing and insert the code of its preconditions.
92
                if (is_array($tokens[$i]) && $tokens[$i][0] === T_FUNCTION && is_array($tokens[$i + 2])) {
93
                    // get the name of the function
94
                    $functionName = $tokens[$i + 2][1];
95
96
                    // check if we got the function in our list, if not continue
97
                    $functionDefinition = $functionDefinitions->get($functionName);
98
99
                    if (!$functionDefinition instanceof FunctionDefinition) {
100
                        continue;
101
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
102
                    } else {
103
                        // collect all pointcut expressions, advice based as well as directly defined ones
104
                        $pointcutExpressions = $functionDefinition->getPointcutExpressions();
105
                        $pointcutExpressions->attach($this->findAdvicePointcutExpressions($functionDefinition));
106
107
                        if ($pointcutExpressions->count() > 0) {
108
                            // sort all relevant pointcut expressions by their joinpoint code hooks
109
                            $sortedFunctionPointcuts = $this->sortPointcutExpressions($pointcutExpressions);
110
111
                            // get all the callbacks for around advices to build a proper advice chain
112
                            $callbackChain = $this->generateAdviceCallbacks($sortedFunctionPointcuts, $functionDefinition);
113
114
                            // before we weave in any advice code we have to make a MethodInvocation object ready
115
                            $this->injectInvocationCode($bucket->data, $functionDefinition, $callbackChain);
116
117
                            // as we need the result of the method invocation we have to collect it
118
                            $this->injectResultInjection($bucket->data, $functionName);
119
120
                            // we need the exception (if any) within our method invocation object
121
                            $this->injectExceptionInjection($bucket->data, $functionName);
122
123
                            // inject the advice code
124
                            $this->injectAdviceCode($bucket->data, $sortedFunctionPointcuts, $functionDefinition);
125
                        }
126
127
                        // "destroy" function definition
128
                        $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...
129
                    }
130
                }
131
            }
132
133
            // tell them how much we already processed, and stuff it back into the output
134
            $consumed += $bucket->datalen;
135
            stream_bucket_append($out, $bucket);
136
        }
137
138
        return PSFS_PASS_ON;
139
    }
140
141
    /**
142
     * Will inject the advice code for the different join-points based on sorted join-points
143
     *
144
     * @param string                                                             $bucketData                Reference on the current bucket's data
145
     * @param array                                                              $sortedPointcutExpressions Array of pointcut expressions sorted by join-points
146
     * @param \AppserverIo\Doppelgaenger\Entities\Definitions\FunctionDefinition $functionDefinition        Name of the function to inject the advices into
147
     *
148
     * @return boolean
149
     */
150
    protected function injectAdviceCode(& $bucketData, array $sortedPointcutExpressions, $functionDefinition)
151
    {
152
        // iterate over the sorted pointcuts and insert the code
153
        $functionName = $functionDefinition->getName();
154
        foreach ($sortedPointcutExpressions as $joinpoint => $pointcutExpressions) {
155
            // only do something if we got expressions
156
            if (empty($pointcutExpressions)) {
157
                continue;
158
            }
159
160
            // get placeholder and replacement prefix based on join-point
161
            $placeholderName = strtoupper($joinpoint) . '_JOINPOINT';
162
            $placeholderHook = constant('\AppserverIo\Doppelgaenger\Dictionaries\Placeholders::' . $placeholderName) .
163
                $functionName . Placeholders::PLACEHOLDER_CLOSE;
164
165
            // around advices have to be woven differently
166
            if ($joinpoint === Around::ANNOTATION && !$pointcutExpressions[0]->getPointcut() instanceof PointcutPointcut) {
167
                // insert the code but make sure to inject only the first one in the row, as the advice chain will
168
                // be implemented via the advice chain
169
                $pointcutExpression = $pointcutExpressions[0];
170
                $bucketData = str_replace(
171
                    $placeholderHook,
172
                    $pointcutExpression->toCode(),
173
                    $bucketData
174
                );
175
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
176
            } else {
177
                // iterate all the others and inject the code
178
                $parameterAssignmentCode = '';
179
                foreach ($pointcutExpressions as $pointcutExpression) {
180
                    if ($joinpoint === Before::ANNOTATION) {
181
                        // before advices have to be woven differently as we have to take changes of the call parameters into account
182
183
                        // we have to build up an assignment of the potentially altered parameters in our method invocation object
184
                        // to the original parameters of the method call.
185
                        // This way we can avoid e.g. broken references by func_get_args and other problems
186
                        $parameterNames = array();
187
                        foreach ($functionDefinition->getParameterDefinitions() as $parameterDefinition) {
188
                            $parameterNames[] = $parameterDefinition->name;
189
                        }
190
                        $parameterAssignmentCode = 'list(' .
191
                            implode(',', $parameterNames) .
192
                            ') = array_values(' . ReservedKeywords::METHOD_INVOCATION_OBJECT . '->getParameters());';
193
194
                        // insert the actual before code
195
                        $bucketData = str_replace(
196
                            $placeholderHook,
197
                            $pointcutExpression->toCode() . $placeholderHook,
198
                            $bucketData
199
                        );
200
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
201
                    } else {
202
                        // all join-point NOT Before or Around can be woven very simply
203
                        $bucketData = str_replace(
204
                            $placeholderHook,
205
                            $placeholderHook . $pointcutExpression->toCode(),
206
                            $bucketData
207
                        );
208
                    }
209
                }
210
211
                // if the before join-point has been used we also have a parameter assignment code we have to insert
212
                if (!empty($parameterAssignmentCode)) {
213
                    $bucketData = str_replace(
214
                        $placeholderHook,
215
                        $parameterAssignmentCode . $placeholderHook,
216
                        $bucketData
217
                    );
218
                }
219
            }
220
        }
221
222
        return true;
223
    }
224
225
    /**
226
     * Will inject the result of the original method invocation into our method invocation object as we need it for later use
227
     *
228
     * @param string $bucketData   Reference on the current bucket's data
229
     * @param string $functionName Name of the function to inject the advices into
230
     *
231
     * @return boolean
232
     */
233 View Code Duplication
    protected function injectResultInjection(& $bucketData, $functionName)
234
    {
235
        $placeholderHook = Placeholders::AROUND_JOINPOINT . $functionName . Placeholders::PLACEHOLDER_CLOSE;
236
        $bucketData = str_replace(
237
            $placeholderHook,
238
            $placeholderHook . '
239
            ' . ReservedKeywords::METHOD_INVOCATION_OBJECT . '->injectResult(' . ReservedKeywords::RESULT . ');',
240
            $bucketData
241
        );
242
    }
243
244
    /**
245
     * Will inject the injection of any thrown exception into our method invocation object as we need it for later use
246
     *
247
     * @param string $bucketData   Reference on the current bucket's data
248
     * @param string $functionName Name of the function to inject the advices into
249
     *
250
     * @return boolean
251
     */
252 View Code Duplication
    protected function injectExceptionInjection(& $bucketData, $functionName)
253
    {
254
        $placeholderHook = Placeholders::AFTERTHROWING_JOINPOINT . $functionName . Placeholders::PLACEHOLDER_CLOSE;
255
        $bucketData = str_replace(
256
            $placeholderHook,
257
            ReservedKeywords::METHOD_INVOCATION_OBJECT . '->injectThrownException(' . ReservedKeywords::THROWN_EXCEPTION_OBJECT . ');
258
            ' . $placeholderHook,
259
            $bucketData
260
        );
261
    }
262
263
    /**
264
     * Will look at all advices known to the aspect register and filter out pointcut expressions matching the
265
     * function in question
266
     * Will return a list of pointcut expressions including distinctive pointcuts to weave in the associated advices
267
     *
268
     * @param FunctionDefinition $functionDefinition Definition of the function to match known advices against
269
     *
270
     * @return \AppserverIo\Doppelgaenger\Entities\Lists\PointcutExpressionList
271
     */
272
    protected function findAdvicePointcutExpressions(FunctionDefinition $functionDefinition)
273
    {
274
        // we have to search for all advices, all of their pointcuts and all pointcut expressions those reference
275
        $pointcutExpressions = new PointcutExpressionList();
276
        foreach ($this->aspectRegister as $aspect) {
277
            foreach ($aspect->getAdvices() as $advice) {
278
                foreach ($advice->getPointcuts() as $pointcut) {
279
                    // there should be no other pointcuts than those referencing pointcut definitions
280
                    if (!$pointcut instanceof PointcutPointcut) {
281
                        continue;
282
                    }
283
284
                    foreach ($pointcut->getReferencedPointcuts() as $referencedPointcut) {
285
                        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...
286
                            // we found a pointcut of an advice that matches!
287
                            // lets create a distinctive join-point and add the advice weaving to the pointcut.
288
                            // Make a clone so that there are no weird reference shenanigans
289
                            $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...
290
                            $joinpoint = new Joinpoint();
291
                            $joinpoint->setCodeHook($advice->getCodeHook());
292
                            $joinpoint->setStructure($functionDefinition->getStructureName());
293
                            $joinpoint->setTarget(Joinpoint::TARGET_METHOD);
294
                            $joinpoint->setTargetName($functionDefinition->getName());
295
296
                            $pointcutExpression->setJoinpoint($joinpoint);
297
298
                            // "straighten out" structure and function referenced by the pointcut to avoid regex within generated code.
299
                            // Same here with the cloning: we do not want to influence the matching process with working on references
300
                            $expressionPointcut = clone $pointcutExpression->getPointcut();
301
                            $expressionPointcut->straightenExpression($functionDefinition);
302
303
                            // add the weaving pointcut into the expression
304
                            $pointcutExpression->setPointcut(new AndPointcut(
305
                                AdvisePointcut::TYPE . str_replace('\\\\', '\\', '(\\' . $advice->getQualifiedName() . ')'),
306
                                $expressionPointcut->getType() . '(' . $expressionPointcut->getExpression() . ')'
307
                            ));
308
                            $pointcutExpression->setString($expressionPointcut->getExpression());
309
310
                            // add it to our result list
311
                            $pointcutExpressions->add($pointcutExpression);
312
313
                            // break here as we only need one, they are implicitly "or" combined
314
                            break;
315
                        }
316
                    }
317
                }
318
            }
319
        }
320
321
        return $pointcutExpressions;
322
    }
323
324
    /**
325
     * Will inject invocation code for a given function into a given piece of code.
326
     * Invocation code will be the instantiation of a \AppserverIo\Doppelgaenger\Entities\MethodInvocation object
327
     * as a basic representation of the given function
328
     *
329
     * @param string             $bucketData         Reference on the current bucket's data
330
     * @param FunctionDefinition $functionDefinition Definition of the function to inject invocation code into
331
     * @param array              $callbackChain      Chain of callbacks which is used to recursively chain calls
332
     *
333
     * @return boolean
334
     */
335
    protected function injectInvocationCode(& $bucketData, FunctionDefinition $functionDefinition, array $callbackChain)
336
    {
337
338
        // start building up the code
339
        $code = '
340
            ' . ReservedKeywords::METHOD_INVOCATION_OBJECT . ' = new \AppserverIo\Doppelgaenger\Entities\MethodInvocation(
341
            ';
342
343
        // add the original method call to the callback chain so it can be integrated, add it and add the context
344
        if ($functionDefinition->isStatic()) {
345
            $contextCode = '__CLASS__';
346
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
347
        } else {
348
            $contextCode = '$this';
349
        }
350
351
        // iterate the callback chain and build up the code but pop the first element as we will invoke it initially
352
        unset($callbackChain[0]);
353
354
        // empty chain? Add the original function at least
355
        if (empty($callbackChain)) {
356
            $callbackChain[] = array($functionDefinition->getStructureName(), $functionDefinition->getName());
357
        }
358
359
        $code .= '    array(';
360
        foreach ($callbackChain as $callback) {
361
            // do some brushing up for the structure
362
            $structure = $callback[0];
363
            if ($structure === $functionDefinition->getStructureName()) {
364
                $structure = $contextCode;
365
            }
366
367
            // also brush up the function call to direct to the original
368
            if ($callback[1] === $functionDefinition->getName()) {
369
                $callback[1] = $functionDefinition->getName() . ReservedKeywords::ORIGINAL_FUNCTION_SUFFIX;
370
            }
371
372
            $code .= 'array(' . $structure . ', \'' . $callback[1] . '\'),';
373
        }
374
        $code .= '),
375
        ';
376
377
        // continue with the access modifiers
378
        $code .= '        ' . $contextCode . ',
379
                ' . ($functionDefinition->isAbstract() ? 'true' : 'false') . ',
380
                ' . ($functionDefinition->isFinal() ? 'true' : 'false') . ',
381
                ' . ($functionDefinition->isStatic() ? 'true' : 'false') . ',
382
            ';
383
384
        // we have to build up manual parameter collection as func_get_args() only returns copies
385
        // @see http://php.net/manual/en/function.func-get-args.php
386
        $parametersCode = '    array(';
387
        foreach ($functionDefinition->getParameterDefinitions() as $parameterDefinition) {
388
            $name = $parameterDefinition->name;
389
            $parametersCode .= '\'' . substr($name, 1) . '\' => ' . $name . ',';
390
        }
391
        $parametersCode .= ')';
392
393
        $code .= '    \'' . $functionDefinition->getName() . '\',
394
            ' .$parametersCode . ',
395
                __CLASS__,
396
                \'' . $functionDefinition->getVisibility() . '\'
397
            );';
398
399
        // Insert the code
400
        $placeholder = Placeholders::FUNCTION_BEGIN . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE;
401
        $bucketData = str_replace(
402
            $placeholder,
403
            $placeholder . $code,
404
            $bucketData
405
        );
406
407
        return true;
408
    }
409
410
    /**
411
     * Will generate and advice chain of callbacks to the given around pointcut expressions
412
     *
413
     * @param array              $sortedPointcutExpressions Pointcut expressions sorted by their join-point's code hooks
414
     * @param FunctionDefinition $functionDefinition        Definition of the function to inject invocation code into
415
     *
416
     * @return array
417
     */
418
    protected function generateAdviceCallbacks(array $sortedPointcutExpressions, FunctionDefinition $functionDefinition)
419
    {
420
421
        // collect the callback chains of the involved pointcut expressions
422
        $callbackChain = array();
423
        if (isset($sortedPointcutExpressions[Around::ANNOTATION])) {
424
            foreach ($sortedPointcutExpressions[Around::ANNOTATION] as $aroundExpression) {
425
                $callbackChain = array_merge($callbackChain, $aroundExpression->getPointcut()->getCallbackChain($functionDefinition));
426
            }
427
        }
428
429
        // filter the combined callback chain to avoid doubled calls to the original implementation
430
        $callbackChainCount = (count($callbackChain) - 1);
431
        for ($i = 0; $i < $callbackChainCount; $i ++) {
432
            if ($callbackChain[$i][1] === $functionDefinition->getName()) {
433
                unset($callbackChain[$i]);
434
            }
435
        }
436
437
        return $callbackChain;
438
    }
439
440
    /**
441
     * Will sort a list of given pointcut expressions based on the joinpoints associated with them
442
     *
443
     * @param \AppserverIo\Doppelgaenger\Entities\Lists\PointcutExpressionList $pointcutExpressions List of pointcut
444
     *          expressions
445
     *
446
     * @return array
447
     */
448
    protected function sortPointcutExpressions($pointcutExpressions)
449
    {
450
        // sort by join-point code hooks
451
        $sortedPointcutExpressions = array();
452
        foreach ($pointcutExpressions as $pointcutExpression) {
453
            $sortedPointcutExpressions[$pointcutExpression->getJoinpoint()->getCodeHook()][] = $pointcutExpression;
454
        }
455
456
        return $sortedPointcutExpressions;
457
    }
458
}
459