ProxyMockFactory   A
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 202
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
wmc 40
lcom 1
cbo 0
dl 0
loc 202
rs 9.2
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A create() 0 15 2
B renderMethod() 0 39 10
F getMethodParameters() 0 69 21
A renderClass() 0 34 5
A evalClass() 0 6 2

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace timesplinter\ProxyMock;
4
5
/**
6
 * Class ServiceMockFactory
7
 * @package timesplinter\ServiceMock
8
 */
9
final class ProxyMockFactory implements ProxyMockFactoryInterface
10
{
11
12
    /**
13
     * @var array|string[]
14
     */
15
    private $registry = [];
16
17
    /**
18
     * @param string $className
19
     * @return ProxyMockInterface
20
     */
21
    public function create(string $className): ProxyMockInterface
22
    {
23
        if (false === isset($this->registry[$className])) {
24
            $reflector = new \ReflectionClass($className);
25
26
            $proxyClassName = 'ProxyMock_' . uniqid() . '_' . $reflector->getShortName();
27
            $classCode = $this->renderClass($reflector, $proxyClassName);
28
29
            $this->evalClass($classCode, $proxyClassName);
30
31
            $this->registry[$className] = $proxyClassName;
32
        }
33
34
        return new $this->registry[$className]();
35
    }
36
37
    /**
38
     * @param \ReflectionMethod $method
39
     * @return string
40
     */
41
    private function renderMethod(\ReflectionMethod $method)
42
    {
43
        $methodBody = file_get_contents(__DIR__.'/Generator/method.tpl');
44
45
        $modifier = '';
46
47
        if ($method->isFinal()) {
48
            throw new \RuntimeException(
49
                sprintf(
50
                    'Can not proxy final method %s of class %s',
51
                    $method->name,
52
                    $method->class
53
                )
54
            );
55
        }
56
57
        if ($method->isPublic()) {
58
            $modifier .= 'public ';
59
        } elseif ($method->isProtected()) {
60
            $modifier .= 'protected ';
61
        } elseif ($method->isPrivate()) {
62
            $modifier .= 'private ';
63
        }
64
65
        if ($method->isStatic()) {
66
            $modifier .= 'static ';
67
        }
68
69
        $argumentsCall = $this->getMethodParameters($method, true);
70
71
        return strtr($methodBody, [
72
            '<modifier>' => trim($modifier),
73
            '<methodName>' => $method->name,
74
            '<arguments>' => $this->getMethodParameters($method),
75
            '<argumentsCall>' => '' !== $argumentsCall ? $argumentsCall : null,
76
            '<return>' => $method->hasReturnType() && 'void' === $method->getReturnType()->getName() ? null : 'return ',
77
            '<returnType>' => $method->hasReturnType() ? ': '.$method->getReturnType() : null
78
        ]);
79
    }
80
81
    /**
82
     * Returns the parameters of a function or method.
83
     * From https://github.com/sebastianbergmann/phpunit-mock-objects
84
     *
85
     * @param \ReflectionMethod $method
86
     * @param bool             $forCall
87
     *
88
     * @return string
89
     */
90
    private function getMethodParameters(\ReflectionMethod $method, $forCall = false)
91
    {
92
        $parameters = [];
93
        foreach ($method->getParameters() as $i => $parameter) {
94
            $name = '$' . $parameter->name;
95
            /* Note: PHP extensions may use empty names for reference arguments
96
             * or "..." for methods taking a variable number of arguments.
97
             */
98
            if ($name === '$' || $name === '$...') {
99
                $name = '$arg' . $i;
100
            }
101
            if ($parameter->isVariadic()) {
102
                if ($forCall) {
103
                    continue;
104
                }
105
                $name = '...' . $name;
106
            }
107
            $nullable        = '';
108
            $default         = '';
109
            $reference       = '';
110
            $typeDeclaration = '';
111
            if (!$forCall) {
112
                if ($parameter->hasType() && (string) $parameter->getType() !== 'self') {
113
                    if (version_compare(PHP_VERSION, '7.1', '>=')
114
                        && $parameter->allowsNull()
115
                        && !$parameter->isVariadic()
116
                    ) {
117
                        $nullable = '?';
118
                    }
119
                    $typeDeclaration = (string) $parameter->getType() . ' ';
120
                } elseif ($parameter->isArray()) {
121
                    $typeDeclaration = 'array ';
122
                } elseif ($parameter->isCallable()) {
123
                    $typeDeclaration = 'callable ';
124
                } else {
125
                    try {
126
                        $class = $parameter->getClass();
127
                    } catch (\ReflectionException $e) {
128
                        throw new \RuntimeException(
129
                            sprintf(
130
                                'Cannot mock %s::%s() because a class or ' .
131
                                'interface used in the signature is not loaded',
132
                                $method->class,
133
                                $method->name
134
                            ),
135
                            0,
136
                            $e
137
                        );
138
                    }
139
                    if ($class !== null) {
140
                        $typeDeclaration = $class->name . ' ';
141
                    }
142
                }
143
                if (!$parameter->isVariadic()) {
144
                    if ($parameter->isDefaultValueAvailable()) {
145
                        $value   = $parameter->getDefaultValue();
146
                        $default = ' = ' . var_export($value, true);
147
                    } elseif ($parameter->isOptional()) {
148
                        $default = ' = null';
149
                    }
150
                }
151
            }
152
            if (false === $forCall && $parameter->isPassedByReference()) {
153
                $reference = '&';
154
            }
155
            $parameters[] = $nullable . $typeDeclaration . $reference . $name . $default;
156
        }
157
        return implode(', ', $parameters);
158
    }
159
160
    /**
161
     * @param \ReflectionClass $class
162
     * @param string $proxyClassName
163
     * @return string
164
     */
165
    private function renderClass(\ReflectionClass $class, string $proxyClassName)
166
    {
167
168
        $classDeclaration = $proxyClassName;
169
170
        if ($class->isFinal()) {
171
            throw new \RuntimeException(
172
                sprintf('Class %s is final and can therefor not be proxied', $class->name)
173
            );
174
        } elseif ($class->isInterface()) {
175
            $classDeclaration .= ' implements ' . $class->name .', ' . ProxyMockInterface::class;
176
        } else {
177
            $classDeclaration .= ' extends ' . $class->name . ' implements ' . ProxyMockInterface::class;
178
        }
179
180
        $methodCode = '';
181
182
        foreach ($class->getMethods() as $method)
183
        {
184
            if (true === $method->isConstructor()) {
185
                continue;
186
            }
187
188
            $methodCode .= $this->renderMethod($method) . PHP_EOL;
189
        }
190
191
        $classBody = file_get_contents(__DIR__ . '/Generator/class.tpl');
192
193
        return strtr($classBody, [
194
            '<classDeclaration>' => $classDeclaration,
195
            '<originalClassName>' => $class->name,
196
            '<methods>' => trim($methodCode),
197
        ]);
198
    }
199
200
    /**
201
     * @param string $code
202
     * @param string $className
203
     */
204
    private function evalClass($code, $className)
205
    {
206
        if (!class_exists($className, false)) {
207
            eval($code);
208
        }
209
    }
210
}
211