Passed
Branch master (203e41)
by Nate
01:56
created

DefaultProxyFactory   B

Complexity

Total Complexity 20

Size/Duplication

Total Lines 207
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 16

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 20
lcom 1
cbo 16
dl 0
loc 207
ccs 82
cts 82
cp 1
rs 8.4614
c 0
b 0
f 0

2 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 17 1
F create() 0 127 19
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\ConstFetch;
15
use PhpParser\Node\Expr\FuncCall;
16
use PhpParser\Node\Expr\MethodCall;
17
use PhpParser\Node\Expr\Variable;
18
use PhpParser\Node\Name;
19
use PhpParser\Node\NullableType;
20
use PhpParser\Node\Scalar\String_;
21
use PhpParser\Node\Stmt\Return_;
22
use PhpParser\PrettyPrinterAbstract;
23
use ReflectionClass;
24
use RuntimeException;
25
use Tebru\PhpType\TypeToken;
26
use Tebru\Retrofit\HttpClient;
27
use Tebru\Retrofit\Internal\ServiceMethod\ServiceMethodFactory;
28
use Tebru\Retrofit\Proxy;
29
use Tebru\Retrofit\Proxy\AbstractProxy;
30
use Tebru\Retrofit\ProxyFactory;
31
32
/**
33
 * Class DefaultProxyFactory
34
 *
35
 * @author Nate Brunette <[email protected]>
36
 */
37
final class DefaultProxyFactory implements ProxyFactory
38
{
39
    public const PROXY_PREFIX = 'Tebru\Retrofit\Proxy\\';
40
41
    /**
42
     * @var BuilderFactory
43
     */
44
    private $builderFactory;
45
46
    /**
47
     * @var PrettyPrinterAbstract
48
     */
49
    private $printer;
50
51
    /**
52
     * @var ServiceMethodFactory
53
     */
54
    private $serviceMethodFactory;
55
56
    /**
57
     * @var HttpClient
58
     */
59
    private $httpClient;
60
61
    /**
62
     * @var Filesystem
63
     */
64
    private $filesystem;
65
66
    /**
67
     * @var bool
68
     */
69
    private $enableCache;
70
71
    /**
72
     * @var string
73
     */
74
    private $cacheDir;
75
76
    /**
77
     * Constructor
78
     *
79
     * @param BuilderFactory $builderFactory
80
     * @param PrettyPrinterAbstract $printer
81
     * @param ServiceMethodFactory $serviceMethodFactory
82
     * @param HttpClient $httpClient
83
     * @param Filesystem $filesystem
84
     * @param bool $enableCache
85
     * @param string $cacheDir
86
     */
87 22
    public function __construct(
88
        BuilderFactory $builderFactory,
89
        PrettyPrinterAbstract $printer,
90
        ServiceMethodFactory $serviceMethodFactory,
91
        HttpClient $httpClient,
92
        Filesystem $filesystem,
93
        bool $enableCache,
94
        string $cacheDir
95
    ) {
96 22
        $this->builderFactory = $builderFactory;
97 22
        $this->printer = $printer;
98 22
        $this->serviceMethodFactory = $serviceMethodFactory;
99 22
        $this->httpClient = $httpClient;
100 22
        $this->filesystem = $filesystem;
101 22
        $this->enableCache = $enableCache;
102 22
        $this->cacheDir = $cacheDir;
103 22
    }
104
105
    /**
106
     * Create a new proxy class given an interface name. This returns a class
107
     * in a string to be cached.
108
     *
109
     * @param string $service
110
     * @return Proxy
111
     * @throws \RuntimeException
112
     * @throws \LogicException
113
     * @throws \Tebru\PhpType\Exception\MalformedTypeException
114
     * @throws \InvalidArgumentException
115
     */
116 22
    public function create(string $service): ?Proxy
117
    {
118 22
        $className = self::PROXY_PREFIX.$service;
119 22
        if ($this->enableCache && class_exists($className)) {
120 1
            return new $className($this->serviceMethodFactory, $this->httpClient);
121
        }
122
123 22
        if (!$this->enableCache && class_exists($className, false)) {
124 13
            return new $className($this->serviceMethodFactory, $this->httpClient);
125
        }
126
127 11
        if (!interface_exists($service)) {
128 1
            throw new InvalidArgumentException(sprintf('Retrofit: %s is expected to be an interface', $service));
129
        }
130
131
        /** @noinspection ExceptionsAnnotatingAndHandlingInspection */
132 10
        $reflectionClass = new ReflectionClass($service);
133 10
        $builder = $this->builderFactory
134 10
            ->class($reflectionClass->getShortName())
135 10
            ->extend('\\'.AbstractProxy::class)
136 10
            ->implement('\\'.$reflectionClass->getName());
0 ignored issues
show
Bug introduced by
Consider using $reflectionClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
137
138 10
        foreach ($reflectionClass->getMethods() as $reflectionMethod) {
139 10
            $methodBuilder = $this->builderFactory
140 10
                ->method($reflectionMethod->getName())
0 ignored issues
show
Bug introduced by
Consider using $reflectionMethod->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
141 10
                ->makePublic();
142
143 10
            if ($reflectionMethod->isStatic()) {
144 5
                $methodBuilder->makeStatic();
145
            }
146
147 10
            foreach ($reflectionMethod->getParameters() as $reflectionParameter) {
148 9
                $paramBuilder = $this->builderFactory->param($reflectionParameter->getName());
0 ignored issues
show
Bug introduced by
Consider using $reflectionParameter->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
149
150 9
                if ($reflectionParameter->isDefaultValueAvailable()) {
151 5
                    $paramBuilder->setDefault($reflectionParameter->getDefaultValue());
152
                }
153
154 9
                if ($reflectionParameter->getType() === null) {
155 1
                    throw new LogicException(sprintf(
156 1
                        'Retrofit: Parameter types are required. None found for parameter %s in %s::%s()',
157 1
                        $reflectionParameter->getName(),
0 ignored issues
show
Bug introduced by
Consider using $reflectionParameter->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
158 1
                        $reflectionClass->getName(),
0 ignored issues
show
Bug introduced by
Consider using $reflectionClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
159 1
                        $reflectionMethod->getName()
0 ignored issues
show
Bug introduced by
Consider using $reflectionMethod->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
160
                    ));
161
                }
162
163 8
                $reflectionTypeName = $reflectionParameter->getType()->getName();
164 8
                if ((new TypeToken($reflectionTypeName))->isObject()) {
165 8
                    $reflectionTypeName = '\\'.$reflectionTypeName;
166
                }
167
168 8
                $type = $reflectionParameter->getType()->allowsNull() ? new NullableType($reflectionTypeName): $reflectionTypeName;
169 8
                $paramBuilder->setTypeHint($type);
170
171 8
                if ($reflectionParameter->isPassedByReference()) {
172 5
                    $paramBuilder->makeByRef();
173
                }
174
175 8
                if ($reflectionParameter->isVariadic()) {
176 5
                    $paramBuilder->makeVariadic();
177
                }
178
179 8
                $methodBuilder->addParam($paramBuilder->getNode());
180
            }
181
182 9
            if (!$reflectionMethod->hasReturnType()) {
183 1
                throw new LogicException(sprintf(
184 1
                    'Retrofit: Method return types are required. None found for %s::%s()',
185 1
                    $reflectionClass->getName(),
0 ignored issues
show
Bug introduced by
Consider using $reflectionClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
186 1
                    $reflectionMethod->getName()
0 ignored issues
show
Bug introduced by
Consider using $reflectionMethod->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
187
                ));
188
            }
189
190
            /** @noinspection NullPointerExceptionInspection */
191 8
            $methodBuilder->setReturnType('\\'.$reflectionMethod->getReturnType()->getName());
192
193 8
            $methodBuilder->addStmt(
194 8
                new Return_(
195 8
                    new MethodCall(
196 8
                        new Variable('this'),
197 8
                        '__handleRetrofitRequest',
198
                        [
199 8
                            new String_($reflectionClass->getName()),
0 ignored issues
show
Bug introduced by
Consider using $reflectionClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
200 8
                            new ConstFetch(new Name('__FUNCTION__')),
201 8
                            new FuncCall(new Name('func_get_args'))
202
                        ]
203
                    )
204
                )
205
            );
206
207 8
            $builder->addStmt($methodBuilder->getNode());
208
        }
209
210
211 8
        $namespaceBuilder = $this->builderFactory
212 8
            ->namespace(self::PROXY_PREFIX.$reflectionClass->getNamespaceName())
213 8
            ->addStmt($builder);
214
215 8
        $source = $this->printer->prettyPrint([$namespaceBuilder->getNode()]);
216
217 8
        eval($source);
218
219 8
        if (!$this->enableCache) {
220 3
            return new $className($this->serviceMethodFactory, $this->httpClient);
221
        }
222
223
        /** @noinspection ExceptionsAnnotatingAndHandlingInspection */
224 5
        $reflectionClass = new ReflectionClass($className);
225 5
        $directory = $this->cacheDir.DIRECTORY_SEPARATOR.$reflectionClass->getNamespaceName();
226 5
        $directory = str_replace('\\', DIRECTORY_SEPARATOR, $directory);
227 5
        $filename = $directory.DIRECTORY_SEPARATOR.$reflectionClass->getShortName().'.php';
228
229 5
        $class = '<?php'.PHP_EOL.PHP_EOL.$source;
230 5
        if (!$this->filesystem->makeDirectory($directory)) {
231 1
            throw new RuntimeException(sprintf(
232 1
                'Retrofit: There was an issue creating the cache directory: %s',
233 1
                $directory)
234
            );
235
        }
236
237 4
        if (!$this->filesystem->put($filename, $class)) {
238 1
            throw new RuntimeException(sprintf('Retrofit: There was an issue writing proxy class to: %s', $filename));
239
        }
240
241 3
        return new $className($this->serviceMethodFactory, $this->httpClient);
242
    }
243
}
244