Completed
Push — master ( cd3029...c1bcb1 )
by Nate
05:05
created

DefaultProxyFactory::mapArray()   D

Complexity

Conditions 10
Paths 3

Size

Total Lines 41
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 10

Importance

Changes 0
Metric Value
dl 0
loc 41
ccs 24
cts 24
cp 1
rs 4.8196
c 0
b 0
f 0
cc 10
eloc 26
nc 3
nop 1
crap 10

How to fix   Complexity   

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
 * Copyright (c) Nate Brunette.
4
 * Distributed under the MIT License (http://opensource.org/licenses/MIT)
5
 */
6
7
declare(strict_types=1);
8
9
namespace Tebru\Retrofit\Internal;
10
11
use InvalidArgumentException;
12
use LogicException;
13
use PhpParser\BuilderFactory;
14
use PhpParser\Node\Expr;
15
use PhpParser\Node\Expr\Array_;
16
use PhpParser\Node\Expr\ConstFetch;
17
use PhpParser\Node\Expr\FuncCall;
18
use PhpParser\Node\Expr\MethodCall;
19
use PhpParser\Node\Expr\Variable;
20
use PhpParser\Node\Name;
21
use PhpParser\Node\NullableType;
22
use PhpParser\Node\Scalar\DNumber;
23
use PhpParser\Node\Scalar\LNumber;
24
use PhpParser\Node\Scalar\String_;
25
use PhpParser\Node\Stmt\Return_;
26
use PhpParser\PrettyPrinterAbstract;
27
use ReflectionClass;
28
use RuntimeException;
29
use Tebru\PhpType\TypeToken;
30
use Tebru\Retrofit\HttpClient;
31
use Tebru\Retrofit\Internal\ServiceMethod\ServiceMethodFactory;
32
use Tebru\Retrofit\Proxy;
33
use Tebru\Retrofit\Proxy\AbstractProxy;
34
use Tebru\Retrofit\ProxyFactory;
35
36
/**
37
 * Class DefaultProxyFactory
38
 *
39
 * @author Nate Brunette <[email protected]>
40
 */
41
final class DefaultProxyFactory implements ProxyFactory
42
{
43
    public const PROXY_PREFIX = 'Tebru\Retrofit\Proxy\\';
44
45
    /**
46
     * @var BuilderFactory
47
     */
48
    private $builderFactory;
49
50
    /**
51
     * @var PrettyPrinterAbstract
52
     */
53
    private $printer;
54
55
    /**
56
     * @var ServiceMethodFactory
57
     */
58
    private $serviceMethodFactory;
59
60
    /**
61
     * @var HttpClient
62
     */
63
    private $httpClient;
64
65
    /**
66
     * @var Filesystem
67
     */
68
    private $filesystem;
69
70
    /**
71
     * @var bool
72
     */
73
    private $enableCache;
74
75
    /**
76
     * @var string
77
     */
78
    private $cacheDir;
79
80
    /**
81
     * Constructor
82
     *
83
     * @param BuilderFactory $builderFactory
84
     * @param PrettyPrinterAbstract $printer
85
     * @param ServiceMethodFactory $serviceMethodFactory
86
     * @param HttpClient $httpClient
87
     * @param Filesystem $filesystem
88
     * @param bool $enableCache
89
     * @param string $cacheDir
90
     */
91 25
    public function __construct(
92
        BuilderFactory $builderFactory,
93
        PrettyPrinterAbstract $printer,
94
        ServiceMethodFactory $serviceMethodFactory,
95
        HttpClient $httpClient,
96
        Filesystem $filesystem,
97
        bool $enableCache,
98
        string $cacheDir
99
    ) {
100 25
        $this->builderFactory = $builderFactory;
101 25
        $this->printer = $printer;
102 25
        $this->serviceMethodFactory = $serviceMethodFactory;
103 25
        $this->httpClient = $httpClient;
104 25
        $this->filesystem = $filesystem;
105 25
        $this->enableCache = $enableCache;
106 25
        $this->cacheDir = $cacheDir;
107 25
    }
108
109
    /**
110
     * Create a new proxy class given an interface name. This returns a class
111
     * in a string to be cached.
112
     *
113
     * @param string $service
114
     * @return Proxy
115
     * @throws \RuntimeException
116
     * @throws \LogicException
117
     * @throws \Tebru\PhpType\Exception\MalformedTypeException
118
     * @throws \InvalidArgumentException
119
     */
120 25
    public function create(string $service): ?Proxy
121
    {
122 25
        $className = self::PROXY_PREFIX.$service;
123 25
        if ($this->enableCache && class_exists($className)) {
124 1
            return new $className($this->serviceMethodFactory, $this->httpClient);
125
        }
126
127 25
        if (!$this->enableCache && class_exists($className, false)) {
128 15
            return new $className($this->serviceMethodFactory, $this->httpClient);
129
        }
130
131 12
        if (!interface_exists($service)) {
132 1
            throw new InvalidArgumentException(sprintf('Retrofit: %s is expected to be an interface', $service));
133
        }
134
135
        /** @noinspection ExceptionsAnnotatingAndHandlingInspection */
136 11
        $reflectionClass = new ReflectionClass($service);
137 11
        $builder = $this->builderFactory
138 11
            ->class($reflectionClass->getShortName())
139 11
            ->extend('\\'.AbstractProxy::class)
140 11
            ->implement('\\'.$reflectionClass->name);
141
142 11
        foreach ($reflectionClass->getMethods() as $reflectionMethod) {
143 11
            $methodBuilder = $this->builderFactory
144 11
                ->method($reflectionMethod->name)
145 11
                ->makePublic();
146
147 11
            if ($reflectionMethod->isStatic()) {
148 5
                $methodBuilder->makeStatic();
149
            }
150
151 11
            $defaultValues = [];
152
153 11
            foreach ($reflectionMethod->getParameters() as $reflectionParameter) {
154 10
                $paramBuilder = $this->builderFactory->param($reflectionParameter->name);
155
156 10
                if ($reflectionParameter->isDefaultValueAvailable()) {
157 6
                    $paramBuilder->setDefault($reflectionParameter->getDefaultValue());
158
                }
159
160 10
                if ($reflectionParameter->getType() === null) {
161 1
                    throw new LogicException(sprintf(
162 1
                        'Retrofit: Parameter types are required. None found for parameter %s in %s::%s()',
163 1
                        $reflectionParameter->name,
164 1
                        $reflectionClass->name,
165 1
                        $reflectionMethod->name
166
                    ));
167
                }
168
169 9
                $reflectionTypeName = (string)$reflectionParameter->getType();
170 9
                if ((new TypeToken($reflectionTypeName))->isObject()) {
171 9
                    $reflectionTypeName = '\\'.$reflectionTypeName;
172
                }
173
174 9
                $type = $reflectionParameter->getType()->allowsNull() ? new NullableType($reflectionTypeName): $reflectionTypeName;
175 9
                $paramBuilder->setTypeHint($type);
176
177 9
                if ($reflectionParameter->isPassedByReference()) {
178 5
                    $paramBuilder->makeByRef();
179
                }
180
181 9
                if ($reflectionParameter->isVariadic()) {
182 5
                    $paramBuilder->makeVariadic();
183
                }
184
185 9
                $methodBuilder->addParam($paramBuilder->getNode());
186
187
                // set all default values
188
                // if a method is called with two few arguments, a native exception will be thrown
189
                // so we can safely use null as a placeholder here.
190 9
                $defaultValues[] = $reflectionParameter->isDefaultValueAvailable()
191 6
                    ? $reflectionParameter->getDefaultValue()
192 9
                    : null;
193
            }
194
195 10
            if (!$reflectionMethod->hasReturnType()) {
196 1
                throw new LogicException(sprintf(
197 1
                    'Retrofit: Method return types are required. None found for %s::%s()',
198 1
                    $reflectionClass->name,
199 1
                    $reflectionMethod->name
200
                ));
201
            }
202
203
            /** @noinspection NullPointerExceptionInspection */
204 9
            $methodBuilder->setReturnType('\\'.(string)$reflectionMethod->getReturnType());
205
206 9
            $defaultNodes = $this->mapArray($defaultValues);
207
208 9
            $methodBuilder->addStmt(
209 9
                new Return_(
210 9
                    new MethodCall(
211 9
                        new Variable('this'),
212 9
                        '__handleRetrofitRequest',
213
                        [
214 9
                            new String_($reflectionClass->name),
215 9
                            new ConstFetch(new Name('__FUNCTION__')),
216 9
                            new FuncCall(new Name('func_get_args')),
217 9
                            new Array_($defaultNodes)
218
                        ]
219
                    )
220
                )
221
            );
222
223 9
            $builder->addStmt($methodBuilder->getNode());
224
        }
225
226 9
        $namespaceBuilder = $this->builderFactory
227 9
            ->namespace(self::PROXY_PREFIX.$reflectionClass->getNamespaceName())
228 9
            ->addStmt($builder);
229
230 9
        $source = $this->printer->prettyPrint([$namespaceBuilder->getNode()]);
231
232 9
        eval($source);
233
234 9
        if (!$this->enableCache) {
235 4
            return new $className($this->serviceMethodFactory, $this->httpClient);
236
        }
237
238 5
        $directory = $this->cacheDir.DIRECTORY_SEPARATOR.$reflectionClass->getNamespaceName();
239 5
        $directory = str_replace('\\', DIRECTORY_SEPARATOR, $directory);
240 5
        $filename = $directory.DIRECTORY_SEPARATOR.$reflectionClass->getShortName().'.php';
241
242 5
        $class = '<?php'.PHP_EOL.PHP_EOL.$source;
243 5
        if (!$this->filesystem->makeDirectory($directory)) {
244 1
            throw new RuntimeException(sprintf(
245 1
                'Retrofit: There was an issue creating the cache directory: %s',
246 1
                $directory)
247
            );
248
        }
249
250 4
        if (!$this->filesystem->put($filename, $class)) {
251 1
            throw new RuntimeException(sprintf('Retrofit: There was an issue writing proxy class to: %s', $filename));
252
        }
253
254 3
        return new $className($this->serviceMethodFactory, $this->httpClient);
255
    }
256
257
    /**
258
     * Convert array to an array of [@see Expr] to add to builder
259
     *
260
     * @param array $array
261
     * @return Expr[]
262
     */
263
    private function mapArray(array $array): array
264
    {
265
        // for each element in the array, create an Expr object
266 9
        $values = array_values(array_map(function ($value) {
267 8
            $type = TypeToken::createFromVariable($value);
268
            switch ($type) {
269 8
                case TypeToken::STRING:
270 6
                    return new String_($value);
271 8
                case TypeToken::INTEGER:
272 1
                    return new LNumber($value);
273 8
                case TypeToken::FLOAT:
274 1
                    return new DNumber($value);
275 8
                case TypeToken::BOOLEAN:
276 1
                    return $value === true ? new ConstFetch(new Name('true')) : new ConstFetch(new Name('false'));
277 8
                case TypeToken::HASH:
278
                    // recurse if array contains an array
279 1
                    return new Array_($this->mapArray($value));
280 8
                case TypeToken::NULL:
281 8
                    return new ConstFetch(new Name('null'));
282
            }
283 9
        }, $array));
284
285 9
        $keys = \array_keys($array);
286 9
        $isNumericKeys = \count(\array_filter($keys, '\is_string')) === 0;
287
288
        // a 0-indexed array can be returned as-is
289 9
        if ($isNumericKeys) {
290 9
            return $values;
291
        }
292
293
        // if we're dealing with an associative array, run the keys through the mapper
294 1
        $keys = $this->mapArray($keys);
295
296
        // create an array of ArrayItem objects for an associative array
297 1
        $items = [];
298 1
        foreach ($values as $index => $value) {
299 1
            $items[] = new Expr\ArrayItem($value, $keys[$index]);
300
        }
301
302 1
        return $items;
303
    }
304
}
305