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
|
26 |
|
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
|
26 |
|
$this->builderFactory = $builderFactory; |
101
|
26 |
|
$this->printer = $printer; |
102
|
26 |
|
$this->serviceMethodFactory = $serviceMethodFactory; |
103
|
26 |
|
$this->httpClient = $httpClient; |
104
|
26 |
|
$this->filesystem = $filesystem; |
105
|
26 |
|
$this->enableCache = $enableCache; |
106
|
26 |
|
$this->cacheDir = $cacheDir; |
107
|
26 |
|
} |
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
|
26 |
|
public function create(string $service): ?Proxy |
121
|
|
|
{ |
122
|
26 |
|
$className = self::PROXY_PREFIX.$service; |
123
|
26 |
|
if ($this->enableCache && class_exists($className)) { |
124
|
1 |
|
return new $className($this->serviceMethodFactory, $this->httpClient); |
125
|
|
|
} |
126
|
|
|
|
127
|
26 |
|
if (!$this->enableCache && class_exists($className, false)) { |
128
|
16 |
|
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 = $reflectionParameter->getType()->getName(); |
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('\\'.$reflectionMethod->getReturnType()->getName()); |
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( |
245
|
1 |
|
sprintf( |
246
|
1 |
|
'Retrofit: There was an issue creating the cache directory: %s', |
247
|
1 |
|
$directory |
248
|
|
|
) |
249
|
|
|
); |
250
|
|
|
} |
251
|
|
|
|
252
|
4 |
|
if (!$this->filesystem->put($filename, $class)) { |
253
|
1 |
|
throw new RuntimeException(sprintf('Retrofit: There was an issue writing proxy class to: %s', $filename)); |
254
|
|
|
} |
255
|
|
|
|
256
|
3 |
|
return new $className($this->serviceMethodFactory, $this->httpClient); |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* Convert array to an array of [@see Expr] to add to builder |
261
|
|
|
* |
262
|
|
|
* @param array $array |
263
|
|
|
* @return Expr[] |
264
|
|
|
*/ |
265
|
9 |
|
private function mapArray(array $array): array |
266
|
|
|
{ |
267
|
|
|
// for each element in the array, create an Expr object |
268
|
|
|
$values = array_values(array_map(function ($value) { |
269
|
8 |
|
$type = TypeToken::createFromVariable($value); |
270
|
|
|
switch ($type) { |
271
|
8 |
|
case TypeToken::STRING: |
272
|
6 |
|
return new String_($value); |
273
|
8 |
|
case TypeToken::INTEGER: |
274
|
1 |
|
return new LNumber($value); |
275
|
8 |
|
case TypeToken::FLOAT: |
276
|
1 |
|
return new DNumber($value); |
277
|
8 |
|
case TypeToken::BOOLEAN: |
278
|
1 |
|
return $value === true ? new ConstFetch(new Name('true')) : new ConstFetch(new Name('false')); |
279
|
8 |
|
case TypeToken::HASH: |
280
|
|
|
// recurse if array contains an array |
281
|
1 |
|
return new Array_($this->mapArray($value)); |
282
|
8 |
|
case TypeToken::NULL: |
283
|
8 |
|
return new ConstFetch(new Name('null')); |
284
|
|
|
} |
285
|
9 |
|
}, $array)); |
286
|
|
|
|
287
|
9 |
|
$keys = \array_keys($array); |
288
|
9 |
|
$isNumericKeys = \count(\array_filter($keys, '\is_string')) === 0; |
289
|
|
|
|
290
|
|
|
// a 0-indexed array can be returned as-is |
291
|
9 |
|
if ($isNumericKeys) { |
292
|
9 |
|
return $values; |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
// if we're dealing with an associative array, run the keys through the mapper |
296
|
1 |
|
$keys = $this->mapArray($keys); |
297
|
|
|
|
298
|
|
|
// create an array of ArrayItem objects for an associative array |
299
|
1 |
|
$items = []; |
300
|
1 |
|
foreach ($values as $index => $value) { |
301
|
1 |
|
$items[] = new Expr\ArrayItem($value, $keys[$index]); |
302
|
|
|
} |
303
|
|
|
|
304
|
1 |
|
return $items; |
305
|
|
|
} |
306
|
|
|
} |
307
|
|
|
|
This method has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.