Completed
Pull Request — 2.x (#381)
by Alexander
02:19
created

AbstractProxy   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 272
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 99.01%

Importance

Changes 0
Metric Value
wmc 44
lcom 1
cbo 1
dl 0
loc 272
ccs 100
cts 101
cp 0.9901
rs 8.3396
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
__toString() 0 1 ?
A indent() 0 9 1
A getParameters() 0 9 2
B getParameterCode() 0 21 8
A flattenAdvices() 0 13 4
C prepareArgsLine() 0 28 8
C getOverriddenFunction() 0 39 8
C resolveParameterTypeName() 0 24 7
B resolveTypeName() 0 22 5

How to fix   Complexity   

Complex Class

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

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\Core\AspectContainer;
14
use Reflection;
15
use ReflectionFunctionAbstract;
16
use ReflectionMethod;
17
use ReflectionParameter;
18
use ReflectionType;
19
20
/**
21
 * Abstract class for building different proxies
22
 */
23
abstract class AbstractProxy
24
{
25
26
    /**
27
     * Indent for source code
28
     *
29
     * @var int
30
     */
31
    protected $indent = 4;
32
33
    /**
34
     * List of advices that are used for generation of child
35
     *
36
     * @var array
37
     */
38
    protected $advices = [];
39
40
    /**
41
     * PHP expression string for accessing LSB information
42
     *
43
     * @var string
44
     */
45
    protected static $staticLsbExpression = 'static::class';
46
47
    /**
48
     * Constructs an abstract proxy class
49
     *
50
     * @param array $advices List of advices
51
     */
52 4
    public function __construct(array $advices = [])
53
    {
54 4
        $this->advices = $this->flattenAdvices($advices);
55 4
    }
56
57
    /**
58
     * Returns text representation of class
59
     *
60
     * @return string
61
     */
62
    abstract public function __toString();
63
64
    /**
65
     * Indent block of code
66
     *
67
     * @param string $text Non-indented text
68
     *
69
     * @return string Indented text
70
     */
71 4
    protected function indent($text)
72
    {
73 4
        $pad   = str_pad('', $this->indent, ' ');
74 4
        $lines = array_map(function ($line) use ($pad) {
75 4
            return $pad . $line;
76 4
        }, explode("\n", $text));
77
78 4
        return implode("\n", $lines);
79
    }
80
81
    /**
82
     * Returns list of string representation of parameters
83
     *
84
     * @param array|ReflectionParameter[] $parameters List of parameters
85
     *
86
     * @return array
87
     */
88 4
    protected function getParameters(array $parameters)
89
    {
90 4
        $parameterDefinitions = [];
91 4
        foreach ($parameters as $parameter) {
92 2
            $parameterDefinitions[] = $this->getParameterCode($parameter);
93
        }
94
95 4
        return $parameterDefinitions;
96
    }
97
98
    /**
99
     * Return string representation of parameter
100
     *
101
     * @param ReflectionParameter $parameter Reflection parameter
102
     *
103
     * @return string
104
     */
105 2
    protected function getParameterCode(ReflectionParameter $parameter)
106
    {
107 2
        $typeName     = $this->resolveParameterTypeName($parameter);
108 2
        $defaultValue = null;
109 2
        $isDefaultValueAvailable = $parameter->isDefaultValueAvailable();
110 2
        if ($isDefaultValueAvailable) {
111 1
            $defaultValue = var_export($parameter->getDefaultValue(), true);
112 2
        } elseif ($parameter->isOptional() && !$parameter->isVariadic()) {
113
            $defaultValue = 'null';
114
        }
115
        $code = (
116 2
            (empty($typeName) ? '' : "$typeName ") . // Typehint
117 2
            ($parameter->isPassedByReference() ? '&' : '') . // By reference sign
118 2
            ($parameter->isVariadic() ? '...' : '') . // Variadic symbol
119 2
            '$' . // Variable symbol
120 2
            $parameter->name . // Name of the argument
121 2
            ($defaultValue !== null ? (' = ' . $defaultValue) : '') // Default value if present
122
        );
123
124 2
        return $code;
125
    }
126
127
    /**
128
     * Replace concrete advices with list of ids
129
     *
130
     * @param array $advices
131
     *
132
     * @return array flatten list of advices
133
     */
134 4
    private function flattenAdvices(array $advices)
135
    {
136 4
        $flattenAdvices = [];
137 4
        foreach ($advices as $type => $typedAdvices) {
138 4
            foreach ($typedAdvices as $name => $concreteAdvices) {
139 4
                if (is_array($concreteAdvices)) {
140 4
                    $flattenAdvices[$type][$name] = array_keys($concreteAdvices);
141
                }
142
            }
143
        }
144
145 4
        return $flattenAdvices;
146
    }
147
148
    /**
149
     * Prepares a line with args from the method definition
150
     *
151
     * @param ReflectionFunctionAbstract $functionLike
152
     *
153
     * @return string
154
     */
155 4
    protected function prepareArgsLine(ReflectionFunctionAbstract $functionLike)
156
    {
157 4
        $argumentsPart = [];
158 4
        $arguments     = [];
159 4
        $hasOptionals  = false;
160
161 4
        foreach ($functionLike->getParameters() as $parameter) {
162 2
            $byReference  = ($parameter->isPassedByReference() && !$parameter->isVariadic()) ? '&' : '';
163 2
            $hasOptionals = $hasOptionals || $parameter->isOptional();
164
165 2
            $arguments[] = $byReference . '$' . $parameter->name;
166
        }
167
168 4
        $isVariadic = $functionLike->isVariadic();
169 4
        if ($isVariadic) {
170 1
            $argumentsPart[] = array_pop($arguments);
171
        }
172 4
        if (!empty($arguments)) {
173
            // Unshifting to keep correct order
174 2
            $argumentLine = '[' . implode(', ', $arguments) . ']';
175 2
            if ($hasOptionals) {
176 1
                $argumentLine = "\\array_slice($argumentLine, 0, \\func_num_args())";
177
            }
178 2
            array_unshift($argumentsPart, $argumentLine);
179
        }
180
181 4
        return implode(', ', $argumentsPart);
182
    }
183
184
    /**
185
     * Creates a function code from Reflection
186
     *
187
     * @param ReflectionFunctionAbstract $functionLike Reflection for method
188
     * @param string $body Body of method
189
     *
190
     * @return string
191
     */
192 4
    protected function getOverriddenFunction(ReflectionFunctionAbstract $functionLike, $body)
193
    {
194 4
        $returnType    = '';
195 4
        $modifiersLine = '';
196 4
        if (PHP_VERSION_ID >= 70000 && $functionLike->hasReturnType()) {
197 1
            $reflectionType = $functionLike->getReturnType();
198 1
            $typeName       = (string) $reflectionType;
199 1
            $returnType     = $this->resolveTypeName($typeName, $reflectionType);
200
        }
201 4
        if ($functionLike instanceof ReflectionMethod) {
202 4
            $modifiersLine  = implode(' ', Reflection::getModifierNames($functionLike->getModifiers()));
203 4
            $declaringClass = $functionLike->getDeclaringClass();
204
            // Normalization of special `self` and `parent` type hints for methods
205 4
            if ($returnType === 'self') {
206 1
                $returnType = '\\' . $declaringClass->getName() . AspectContainer::AOP_PROXIED_SUFFIX;
207
            }
208 4
            if ($returnType === 'parent') {
209 1
                $returnType = '\\' . $parentClass = $declaringClass->getParentClass()->getName();
210
            }
211
        }
212
213
        $code = (
214 4
            preg_replace('/ {4}|\t/', '', $functionLike->getDocComment()) . "\n" . // Original Doc-block
215 4
            $modifiersLine . // List of modifiers (for methods)
216 4
            ' function ' . // 'function' keyword
217 4
            ($functionLike->returnsReference() ? '&' : '') . // By reference symbol
218 4
            $functionLike->name . // Name of the function
219 4
            '(' . // Start of parameters list
220 4
            implode(', ', $this->getParameters($functionLike->getParameters())) . // List of parameters
221 4
            ')' . // End of parameters list
222 4
            ($returnType ? " : $returnType" : '') . // Return type, if present
223 4
            "\n" .
224 4
            "{\n" . // Start of method body
225 4
            $this->indent($body) . "\n" . // Method body
226 4
            "}\n" // End of method body
227
        );
228
229 4
        return $code;
230
    }
231
232
    /**
233
     * Resolves parameter typehint
234
     *
235
     * @param ReflectionParameter $parameter Instance of reflection parameter
236
     *
237
     * @return string Resolved type name (FQN for classes)
238
     */
239 2
    protected function resolveParameterTypeName(ReflectionParameter $parameter)
240
    {
241 2
        $typeName       = '';
242 2
        $reflectionType = (PHP_VERSION_ID >= 70000 && $parameter->hasType()) ? $parameter->getType() : null;
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class ReflectionParameter as the method hasType() does only exist in the following sub-classes of ReflectionParameter: Go\ParserReflection\ReflectionParameter. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

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

class MyUser extends 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 sub-classes 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 parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
243
        // This method for keeping short-time support for `self`, `parent`, etc for PHP>=5.6
244 2
        if (preg_match('/\<(?:optional|required)\> ([\w\\\]+)/i', (string) $parameter, $matches)) {
245 2
            $typeName = $matches[1];
246
        }
247 2
        if ($typeName === '') {
248 1
            return $typeName;
249
        }
250
251 2
        $typeName       = $this->resolveTypeName($typeName, $reflectionType);
252 2
        $declaringClass = $parameter->getDeclaringClass();
253
        // Normalization of special `self` and `parent` type hints for methods
254 2
        if ($typeName === 'self') {
255 1
            $typeName = '\\' . $declaringClass->getName() . AspectContainer::AOP_PROXIED_SUFFIX;
256
        }
257 2
        if ($typeName === 'parent') {
258 1
            $typeName = '\\' . $parentClass = $declaringClass->getParentClass()->getName();
259
        }
260
261 2
        return $typeName;
262
    }
263
264
    /**
265
     * Resolves type name from
266
     *
267
     * @param string $typeName Raw name of the type
268
     * @param ReflectionType|null $reflectionType Type (if present for PHP7)
269
     *
270
     * @return string
271
     */
272 2
    private function resolveTypeName($typeName, $reflectionType = null)
273
    {
274
        // see \PhpParser\BuilderAbstract::normalizeType source code, plus `self` and `parent`
275 2
        static $builtInTypes = [
276
            'string'   => 1,
277
            'float'    => 1,
278
            'boolean'  => 1,
279
            'integer'  => 1,
280
            'callable' => 1,
281
            'iterable' => 1,
282
            'void'     => 1,
283
            'object'   => 1,
284
            'self'     => 1,
285
            'parent'   => 1
286
        ];
287
288 2
        $isNullable = $reflectionType !== null && PHP_VERSION_ID >= 70100 && $reflectionType->allowsNull();
289 2
        $isBuiltin  = isset($builtInTypes[$typeName]);
290 2
        $typeName   = ($isNullable ? '?' : '') . ($isBuiltin ? '' : '\\') . $typeName;
291
292 2
        return $typeName;
293
    }
294
}
295