Chris@14
|
1 <?php
|
Chris@14
|
2 /*
|
Chris@14
|
3 * This file is part of the phpunit-mock-objects package.
|
Chris@14
|
4 *
|
Chris@14
|
5 * (c) Sebastian Bergmann <sebastian@phpunit.de>
|
Chris@14
|
6 *
|
Chris@14
|
7 * For the full copyright and license information, please view the LICENSE
|
Chris@14
|
8 * file that was distributed with this source code.
|
Chris@14
|
9 */
|
Chris@14
|
10 namespace PHPUnit\Framework\MockObject;
|
Chris@14
|
11
|
Chris@14
|
12 use Doctrine\Instantiator\Exception\ExceptionInterface as InstantiatorException;
|
Chris@14
|
13 use Doctrine\Instantiator\Instantiator;
|
Chris@14
|
14 use Iterator;
|
Chris@14
|
15 use IteratorAggregate;
|
Chris@14
|
16 use PHPUnit\Framework\Exception;
|
Chris@14
|
17 use PHPUnit\Util\InvalidArgumentHelper;
|
Chris@14
|
18 use ReflectionClass;
|
Chris@14
|
19 use ReflectionException;
|
Chris@14
|
20 use ReflectionMethod;
|
Chris@14
|
21 use SoapClient;
|
Chris@14
|
22 use Text_Template;
|
Chris@14
|
23 use Traversable;
|
Chris@14
|
24
|
Chris@14
|
25 /**
|
Chris@14
|
26 * Mock Object Code Generator
|
Chris@14
|
27 */
|
Chris@14
|
28 class Generator
|
Chris@14
|
29 {
|
Chris@14
|
30 /**
|
Chris@14
|
31 * @var array
|
Chris@14
|
32 */
|
Chris@14
|
33 private static $cache = [];
|
Chris@14
|
34
|
Chris@14
|
35 /**
|
Chris@14
|
36 * @var Text_Template[]
|
Chris@14
|
37 */
|
Chris@14
|
38 private static $templates = [];
|
Chris@14
|
39
|
Chris@14
|
40 /**
|
Chris@14
|
41 * @var array
|
Chris@14
|
42 */
|
Chris@14
|
43 private $blacklistedMethodNames = [
|
Chris@14
|
44 '__CLASS__' => true,
|
Chris@14
|
45 '__DIR__' => true,
|
Chris@14
|
46 '__FILE__' => true,
|
Chris@14
|
47 '__FUNCTION__' => true,
|
Chris@14
|
48 '__LINE__' => true,
|
Chris@14
|
49 '__METHOD__' => true,
|
Chris@14
|
50 '__NAMESPACE__' => true,
|
Chris@14
|
51 '__TRAIT__' => true,
|
Chris@14
|
52 '__clone' => true,
|
Chris@14
|
53 '__halt_compiler' => true,
|
Chris@14
|
54 ];
|
Chris@14
|
55
|
Chris@14
|
56 /**
|
Chris@14
|
57 * Returns a mock object for the specified class.
|
Chris@14
|
58 *
|
Chris@14
|
59 * @param string|string[] $type
|
Chris@14
|
60 * @param array $methods
|
Chris@14
|
61 * @param array $arguments
|
Chris@14
|
62 * @param string $mockClassName
|
Chris@14
|
63 * @param bool $callOriginalConstructor
|
Chris@14
|
64 * @param bool $callOriginalClone
|
Chris@14
|
65 * @param bool $callAutoload
|
Chris@14
|
66 * @param bool $cloneArguments
|
Chris@14
|
67 * @param bool $callOriginalMethods
|
Chris@14
|
68 * @param object $proxyTarget
|
Chris@14
|
69 * @param bool $allowMockingUnknownTypes
|
Chris@14
|
70 *
|
Chris@14
|
71 * @return MockObject
|
Chris@14
|
72 *
|
Chris@14
|
73 * @throws Exception
|
Chris@14
|
74 * @throws RuntimeException
|
Chris@14
|
75 * @throws \PHPUnit\Framework\Exception
|
Chris@14
|
76 * @throws \ReflectionException
|
Chris@14
|
77 */
|
Chris@14
|
78 public function getMock($type, $methods = [], array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $cloneArguments = true, $callOriginalMethods = false, $proxyTarget = null, $allowMockingUnknownTypes = true)
|
Chris@14
|
79 {
|
Chris@14
|
80 if (!\is_array($type) && !\is_string($type)) {
|
Chris@14
|
81 throw InvalidArgumentHelper::factory(1, 'array or string');
|
Chris@14
|
82 }
|
Chris@14
|
83
|
Chris@14
|
84 if (!\is_string($mockClassName)) {
|
Chris@14
|
85 throw InvalidArgumentHelper::factory(4, 'string');
|
Chris@14
|
86 }
|
Chris@14
|
87
|
Chris@14
|
88 if (!\is_array($methods) && null !== $methods) {
|
Chris@14
|
89 throw InvalidArgumentHelper::factory(2, 'array', $methods);
|
Chris@14
|
90 }
|
Chris@14
|
91
|
Chris@14
|
92 if ($type === 'Traversable' || $type === '\\Traversable') {
|
Chris@14
|
93 $type = 'Iterator';
|
Chris@14
|
94 }
|
Chris@14
|
95
|
Chris@14
|
96 if (\is_array($type)) {
|
Chris@14
|
97 $type = \array_unique(
|
Chris@14
|
98 \array_map(
|
Chris@14
|
99 function ($type) {
|
Chris@14
|
100 if ($type === 'Traversable' ||
|
Chris@14
|
101 $type === '\\Traversable' ||
|
Chris@14
|
102 $type === '\\Iterator') {
|
Chris@14
|
103 return 'Iterator';
|
Chris@14
|
104 }
|
Chris@14
|
105
|
Chris@14
|
106 return $type;
|
Chris@14
|
107 },
|
Chris@14
|
108 $type
|
Chris@14
|
109 )
|
Chris@14
|
110 );
|
Chris@14
|
111 }
|
Chris@14
|
112
|
Chris@14
|
113 if (!$allowMockingUnknownTypes) {
|
Chris@14
|
114 if (\is_array($type)) {
|
Chris@14
|
115 foreach ($type as $_type) {
|
Chris@14
|
116 if (!\class_exists($_type, $callAutoload) &&
|
Chris@14
|
117 !\interface_exists($_type, $callAutoload)) {
|
Chris@14
|
118 throw new RuntimeException(
|
Chris@14
|
119 \sprintf(
|
Chris@14
|
120 'Cannot stub or mock class or interface "%s" which does not exist',
|
Chris@14
|
121 $_type
|
Chris@14
|
122 )
|
Chris@14
|
123 );
|
Chris@14
|
124 }
|
Chris@14
|
125 }
|
Chris@14
|
126 } else {
|
Chris@14
|
127 if (!\class_exists($type, $callAutoload) &&
|
Chris@14
|
128 !\interface_exists($type, $callAutoload)
|
Chris@14
|
129 ) {
|
Chris@14
|
130 throw new RuntimeException(
|
Chris@14
|
131 \sprintf(
|
Chris@14
|
132 'Cannot stub or mock class or interface "%s" which does not exist',
|
Chris@14
|
133 $type
|
Chris@14
|
134 )
|
Chris@14
|
135 );
|
Chris@14
|
136 }
|
Chris@14
|
137 }
|
Chris@14
|
138 }
|
Chris@14
|
139
|
Chris@14
|
140 if (null !== $methods) {
|
Chris@14
|
141 foreach ($methods as $method) {
|
Chris@14
|
142 if (!\preg_match('~[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*~', $method)) {
|
Chris@14
|
143 throw new RuntimeException(
|
Chris@14
|
144 \sprintf(
|
Chris@14
|
145 'Cannot stub or mock method with invalid name "%s"',
|
Chris@14
|
146 $method
|
Chris@14
|
147 )
|
Chris@14
|
148 );
|
Chris@14
|
149 }
|
Chris@14
|
150 }
|
Chris@14
|
151
|
Chris@14
|
152 if ($methods !== \array_unique($methods)) {
|
Chris@14
|
153 throw new RuntimeException(
|
Chris@14
|
154 \sprintf(
|
Chris@14
|
155 'Cannot stub or mock using a method list that contains duplicates: "%s" (duplicate: "%s")',
|
Chris@14
|
156 \implode(', ', $methods),
|
Chris@14
|
157 \implode(', ', \array_unique(\array_diff_assoc($methods, \array_unique($methods))))
|
Chris@14
|
158 )
|
Chris@14
|
159 );
|
Chris@14
|
160 }
|
Chris@14
|
161 }
|
Chris@14
|
162
|
Chris@14
|
163 if ($mockClassName !== '' && \class_exists($mockClassName, false)) {
|
Chris@14
|
164 $reflect = new ReflectionClass($mockClassName);
|
Chris@14
|
165
|
Chris@14
|
166 if (!$reflect->implementsInterface(MockObject::class)) {
|
Chris@14
|
167 throw new RuntimeException(
|
Chris@14
|
168 \sprintf(
|
Chris@14
|
169 'Class "%s" already exists.',
|
Chris@14
|
170 $mockClassName
|
Chris@14
|
171 )
|
Chris@14
|
172 );
|
Chris@14
|
173 }
|
Chris@14
|
174 }
|
Chris@14
|
175
|
Chris@14
|
176 if ($callOriginalConstructor === false && $callOriginalMethods === true) {
|
Chris@14
|
177 throw new RuntimeException(
|
Chris@14
|
178 'Proxying to original methods requires invoking the original constructor'
|
Chris@14
|
179 );
|
Chris@14
|
180 }
|
Chris@14
|
181
|
Chris@14
|
182 $mock = $this->generate(
|
Chris@14
|
183 $type,
|
Chris@14
|
184 $methods,
|
Chris@14
|
185 $mockClassName,
|
Chris@14
|
186 $callOriginalClone,
|
Chris@14
|
187 $callAutoload,
|
Chris@14
|
188 $cloneArguments,
|
Chris@14
|
189 $callOriginalMethods
|
Chris@14
|
190 );
|
Chris@14
|
191
|
Chris@14
|
192 return $this->getObject(
|
Chris@14
|
193 $mock['code'],
|
Chris@14
|
194 $mock['mockClassName'],
|
Chris@14
|
195 $type,
|
Chris@14
|
196 $callOriginalConstructor,
|
Chris@14
|
197 $callAutoload,
|
Chris@14
|
198 $arguments,
|
Chris@14
|
199 $callOriginalMethods,
|
Chris@14
|
200 $proxyTarget
|
Chris@14
|
201 );
|
Chris@14
|
202 }
|
Chris@14
|
203
|
Chris@14
|
204 /**
|
Chris@14
|
205 * @param string $code
|
Chris@14
|
206 * @param string $className
|
Chris@14
|
207 * @param array|string $type
|
Chris@14
|
208 * @param bool $callOriginalConstructor
|
Chris@14
|
209 * @param bool $callAutoload
|
Chris@14
|
210 * @param array $arguments
|
Chris@14
|
211 * @param bool $callOriginalMethods
|
Chris@14
|
212 * @param object $proxyTarget
|
Chris@14
|
213 *
|
Chris@14
|
214 * @return MockObject
|
Chris@14
|
215 *
|
Chris@14
|
216 * @throws \ReflectionException
|
Chris@14
|
217 * @throws RuntimeException
|
Chris@14
|
218 */
|
Chris@14
|
219 private function getObject($code, $className, $type = '', $callOriginalConstructor = false, $callAutoload = false, array $arguments = [], $callOriginalMethods = false, $proxyTarget = null)
|
Chris@14
|
220 {
|
Chris@14
|
221 $this->evalClass($code, $className);
|
Chris@14
|
222
|
Chris@14
|
223 if ($callOriginalConstructor &&
|
Chris@14
|
224 \is_string($type) &&
|
Chris@14
|
225 !\interface_exists($type, $callAutoload)) {
|
Chris@14
|
226 if (\count($arguments) === 0) {
|
Chris@14
|
227 $object = new $className;
|
Chris@14
|
228 } else {
|
Chris@14
|
229 $class = new ReflectionClass($className);
|
Chris@14
|
230 $object = $class->newInstanceArgs($arguments);
|
Chris@14
|
231 }
|
Chris@14
|
232 } else {
|
Chris@14
|
233 try {
|
Chris@14
|
234 $instantiator = new Instantiator;
|
Chris@14
|
235 $object = $instantiator->instantiate($className);
|
Chris@14
|
236 } catch (InstantiatorException $exception) {
|
Chris@14
|
237 throw new RuntimeException($exception->getMessage());
|
Chris@14
|
238 }
|
Chris@14
|
239 }
|
Chris@14
|
240
|
Chris@14
|
241 if ($callOriginalMethods) {
|
Chris@14
|
242 if (!\is_object($proxyTarget)) {
|
Chris@14
|
243 if (\count($arguments) === 0) {
|
Chris@14
|
244 $proxyTarget = new $type;
|
Chris@14
|
245 } else {
|
Chris@14
|
246 $class = new ReflectionClass($type);
|
Chris@14
|
247 $proxyTarget = $class->newInstanceArgs($arguments);
|
Chris@14
|
248 }
|
Chris@14
|
249 }
|
Chris@14
|
250
|
Chris@14
|
251 $object->__phpunit_setOriginalObject($proxyTarget);
|
Chris@14
|
252 }
|
Chris@14
|
253
|
Chris@14
|
254 return $object;
|
Chris@14
|
255 }
|
Chris@14
|
256
|
Chris@14
|
257 /**
|
Chris@14
|
258 * @param string $code
|
Chris@14
|
259 * @param string $className
|
Chris@14
|
260 */
|
Chris@14
|
261 private function evalClass($code, $className)
|
Chris@14
|
262 {
|
Chris@14
|
263 if (!\class_exists($className, false)) {
|
Chris@14
|
264 eval($code);
|
Chris@14
|
265 }
|
Chris@14
|
266 }
|
Chris@14
|
267
|
Chris@14
|
268 /**
|
Chris@14
|
269 * Returns a mock object for the specified abstract class with all abstract
|
Chris@14
|
270 * methods of the class mocked. Concrete methods to mock can be specified with
|
Chris@14
|
271 * the last parameter
|
Chris@14
|
272 *
|
Chris@14
|
273 * @param string $originalClassName
|
Chris@14
|
274 * @param array $arguments
|
Chris@14
|
275 * @param string $mockClassName
|
Chris@14
|
276 * @param bool $callOriginalConstructor
|
Chris@14
|
277 * @param bool $callOriginalClone
|
Chris@14
|
278 * @param bool $callAutoload
|
Chris@14
|
279 * @param array $mockedMethods
|
Chris@14
|
280 * @param bool $cloneArguments
|
Chris@14
|
281 *
|
Chris@14
|
282 * @return MockObject
|
Chris@14
|
283 *
|
Chris@14
|
284 * @throws \ReflectionException
|
Chris@14
|
285 * @throws RuntimeException
|
Chris@14
|
286 * @throws Exception
|
Chris@14
|
287 */
|
Chris@14
|
288 public function getMockForAbstractClass($originalClassName, array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = [], $cloneArguments = true)
|
Chris@14
|
289 {
|
Chris@14
|
290 if (!\is_string($originalClassName)) {
|
Chris@14
|
291 throw InvalidArgumentHelper::factory(1, 'string');
|
Chris@14
|
292 }
|
Chris@14
|
293
|
Chris@14
|
294 if (!\is_string($mockClassName)) {
|
Chris@14
|
295 throw InvalidArgumentHelper::factory(3, 'string');
|
Chris@14
|
296 }
|
Chris@14
|
297
|
Chris@14
|
298 if (\class_exists($originalClassName, $callAutoload) ||
|
Chris@14
|
299 \interface_exists($originalClassName, $callAutoload)) {
|
Chris@14
|
300 $reflector = new ReflectionClass($originalClassName);
|
Chris@14
|
301 $methods = $mockedMethods;
|
Chris@14
|
302
|
Chris@14
|
303 foreach ($reflector->getMethods() as $method) {
|
Chris@14
|
304 if ($method->isAbstract() && !\in_array($method->getName(), $methods)) {
|
Chris@14
|
305 $methods[] = $method->getName();
|
Chris@14
|
306 }
|
Chris@14
|
307 }
|
Chris@14
|
308
|
Chris@14
|
309 if (empty($methods)) {
|
Chris@14
|
310 $methods = null;
|
Chris@14
|
311 }
|
Chris@14
|
312
|
Chris@14
|
313 return $this->getMock(
|
Chris@14
|
314 $originalClassName,
|
Chris@14
|
315 $methods,
|
Chris@14
|
316 $arguments,
|
Chris@14
|
317 $mockClassName,
|
Chris@14
|
318 $callOriginalConstructor,
|
Chris@14
|
319 $callOriginalClone,
|
Chris@14
|
320 $callAutoload,
|
Chris@14
|
321 $cloneArguments
|
Chris@14
|
322 );
|
Chris@14
|
323 }
|
Chris@14
|
324
|
Chris@14
|
325 throw new RuntimeException(
|
Chris@14
|
326 \sprintf('Class "%s" does not exist.', $originalClassName)
|
Chris@14
|
327 );
|
Chris@14
|
328 }
|
Chris@14
|
329
|
Chris@14
|
330 /**
|
Chris@14
|
331 * Returns a mock object for the specified trait with all abstract methods
|
Chris@14
|
332 * of the trait mocked. Concrete methods to mock can be specified with the
|
Chris@14
|
333 * `$mockedMethods` parameter.
|
Chris@14
|
334 *
|
Chris@14
|
335 * @param string $traitName
|
Chris@14
|
336 * @param array $arguments
|
Chris@14
|
337 * @param string $mockClassName
|
Chris@14
|
338 * @param bool $callOriginalConstructor
|
Chris@14
|
339 * @param bool $callOriginalClone
|
Chris@14
|
340 * @param bool $callAutoload
|
Chris@14
|
341 * @param array $mockedMethods
|
Chris@14
|
342 * @param bool $cloneArguments
|
Chris@14
|
343 *
|
Chris@14
|
344 * @return MockObject
|
Chris@14
|
345 *
|
Chris@14
|
346 * @throws \ReflectionException
|
Chris@14
|
347 * @throws RuntimeException
|
Chris@14
|
348 * @throws Exception
|
Chris@14
|
349 */
|
Chris@14
|
350 public function getMockForTrait($traitName, array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = [], $cloneArguments = true)
|
Chris@14
|
351 {
|
Chris@14
|
352 if (!\is_string($traitName)) {
|
Chris@14
|
353 throw InvalidArgumentHelper::factory(1, 'string');
|
Chris@14
|
354 }
|
Chris@14
|
355
|
Chris@14
|
356 if (!\is_string($mockClassName)) {
|
Chris@14
|
357 throw InvalidArgumentHelper::factory(3, 'string');
|
Chris@14
|
358 }
|
Chris@14
|
359
|
Chris@14
|
360 if (!\trait_exists($traitName, $callAutoload)) {
|
Chris@14
|
361 throw new RuntimeException(
|
Chris@14
|
362 \sprintf(
|
Chris@14
|
363 'Trait "%s" does not exist.',
|
Chris@14
|
364 $traitName
|
Chris@14
|
365 )
|
Chris@14
|
366 );
|
Chris@14
|
367 }
|
Chris@14
|
368
|
Chris@14
|
369 $className = $this->generateClassName(
|
Chris@14
|
370 $traitName,
|
Chris@14
|
371 '',
|
Chris@14
|
372 'Trait_'
|
Chris@14
|
373 );
|
Chris@14
|
374
|
Chris@14
|
375 $classTemplate = $this->getTemplate('trait_class.tpl');
|
Chris@14
|
376
|
Chris@14
|
377 $classTemplate->setVar(
|
Chris@14
|
378 [
|
Chris@14
|
379 'prologue' => 'abstract ',
|
Chris@14
|
380 'class_name' => $className['className'],
|
Chris@14
|
381 'trait_name' => $traitName
|
Chris@14
|
382 ]
|
Chris@14
|
383 );
|
Chris@14
|
384
|
Chris@14
|
385 $this->evalClass(
|
Chris@14
|
386 $classTemplate->render(),
|
Chris@14
|
387 $className['className']
|
Chris@14
|
388 );
|
Chris@14
|
389
|
Chris@14
|
390 return $this->getMockForAbstractClass($className['className'], $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $mockedMethods, $cloneArguments);
|
Chris@14
|
391 }
|
Chris@14
|
392
|
Chris@14
|
393 /**
|
Chris@14
|
394 * Returns an object for the specified trait.
|
Chris@14
|
395 *
|
Chris@14
|
396 * @param string $traitName
|
Chris@14
|
397 * @param array $arguments
|
Chris@14
|
398 * @param string $traitClassName
|
Chris@14
|
399 * @param bool $callOriginalConstructor
|
Chris@14
|
400 * @param bool $callOriginalClone
|
Chris@14
|
401 * @param bool $callAutoload
|
Chris@14
|
402 *
|
Chris@14
|
403 * @return object
|
Chris@14
|
404 *
|
Chris@14
|
405 * @throws \ReflectionException
|
Chris@14
|
406 * @throws RuntimeException
|
Chris@14
|
407 * @throws Exception
|
Chris@14
|
408 */
|
Chris@14
|
409 public function getObjectForTrait($traitName, array $arguments = [], $traitClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true)
|
Chris@14
|
410 {
|
Chris@14
|
411 if (!\is_string($traitName)) {
|
Chris@14
|
412 throw InvalidArgumentHelper::factory(1, 'string');
|
Chris@14
|
413 }
|
Chris@14
|
414
|
Chris@14
|
415 if (!\is_string($traitClassName)) {
|
Chris@14
|
416 throw InvalidArgumentHelper::factory(3, 'string');
|
Chris@14
|
417 }
|
Chris@14
|
418
|
Chris@14
|
419 if (!\trait_exists($traitName, $callAutoload)) {
|
Chris@14
|
420 throw new RuntimeException(
|
Chris@14
|
421 \sprintf(
|
Chris@14
|
422 'Trait "%s" does not exist.',
|
Chris@14
|
423 $traitName
|
Chris@14
|
424 )
|
Chris@14
|
425 );
|
Chris@14
|
426 }
|
Chris@14
|
427
|
Chris@14
|
428 $className = $this->generateClassName(
|
Chris@14
|
429 $traitName,
|
Chris@14
|
430 $traitClassName,
|
Chris@14
|
431 'Trait_'
|
Chris@14
|
432 );
|
Chris@14
|
433
|
Chris@14
|
434 $classTemplate = $this->getTemplate('trait_class.tpl');
|
Chris@14
|
435
|
Chris@14
|
436 $classTemplate->setVar(
|
Chris@14
|
437 [
|
Chris@14
|
438 'prologue' => '',
|
Chris@14
|
439 'class_name' => $className['className'],
|
Chris@14
|
440 'trait_name' => $traitName
|
Chris@14
|
441 ]
|
Chris@14
|
442 );
|
Chris@14
|
443
|
Chris@14
|
444 return $this->getObject($classTemplate->render(), $className['className']);
|
Chris@14
|
445 }
|
Chris@14
|
446
|
Chris@14
|
447 /**
|
Chris@14
|
448 * @param array|string $type
|
Chris@14
|
449 * @param array $methods
|
Chris@14
|
450 * @param string $mockClassName
|
Chris@14
|
451 * @param bool $callOriginalClone
|
Chris@14
|
452 * @param bool $callAutoload
|
Chris@14
|
453 * @param bool $cloneArguments
|
Chris@14
|
454 * @param bool $callOriginalMethods
|
Chris@14
|
455 *
|
Chris@14
|
456 * @return array
|
Chris@14
|
457 *
|
Chris@14
|
458 * @throws \ReflectionException
|
Chris@14
|
459 * @throws \PHPUnit\Framework\MockObject\RuntimeException
|
Chris@14
|
460 */
|
Chris@14
|
461 public function generate($type, array $methods = null, $mockClassName = '', $callOriginalClone = true, $callAutoload = true, $cloneArguments = true, $callOriginalMethods = false)
|
Chris@14
|
462 {
|
Chris@14
|
463 if (\is_array($type)) {
|
Chris@14
|
464 \sort($type);
|
Chris@14
|
465 }
|
Chris@14
|
466
|
Chris@14
|
467 if ($mockClassName === '') {
|
Chris@14
|
468 $key = \md5(
|
Chris@14
|
469 \is_array($type) ? \implode('_', $type) : $type .
|
Chris@14
|
470 \serialize($methods) .
|
Chris@14
|
471 \serialize($callOriginalClone) .
|
Chris@14
|
472 \serialize($cloneArguments) .
|
Chris@14
|
473 \serialize($callOriginalMethods)
|
Chris@14
|
474 );
|
Chris@14
|
475
|
Chris@14
|
476 if (isset(self::$cache[$key])) {
|
Chris@14
|
477 return self::$cache[$key];
|
Chris@14
|
478 }
|
Chris@14
|
479 }
|
Chris@14
|
480
|
Chris@14
|
481 $mock = $this->generateMock(
|
Chris@14
|
482 $type,
|
Chris@14
|
483 $methods,
|
Chris@14
|
484 $mockClassName,
|
Chris@14
|
485 $callOriginalClone,
|
Chris@14
|
486 $callAutoload,
|
Chris@14
|
487 $cloneArguments,
|
Chris@14
|
488 $callOriginalMethods
|
Chris@14
|
489 );
|
Chris@14
|
490
|
Chris@14
|
491 if (isset($key)) {
|
Chris@14
|
492 self::$cache[$key] = $mock;
|
Chris@14
|
493 }
|
Chris@14
|
494
|
Chris@14
|
495 return $mock;
|
Chris@14
|
496 }
|
Chris@14
|
497
|
Chris@14
|
498 /**
|
Chris@14
|
499 * @param string $wsdlFile
|
Chris@14
|
500 * @param string $className
|
Chris@14
|
501 * @param array $methods
|
Chris@14
|
502 * @param array $options
|
Chris@14
|
503 *
|
Chris@14
|
504 * @return string
|
Chris@14
|
505 *
|
Chris@14
|
506 * @throws RuntimeException
|
Chris@14
|
507 */
|
Chris@14
|
508 public function generateClassFromWsdl($wsdlFile, $className, array $methods = [], array $options = [])
|
Chris@14
|
509 {
|
Chris@14
|
510 if (!\extension_loaded('soap')) {
|
Chris@14
|
511 throw new RuntimeException(
|
Chris@14
|
512 'The SOAP extension is required to generate a mock object from WSDL.'
|
Chris@14
|
513 );
|
Chris@14
|
514 }
|
Chris@14
|
515
|
Chris@14
|
516 $options = \array_merge($options, ['cache_wsdl' => WSDL_CACHE_NONE]);
|
Chris@14
|
517 $client = new SoapClient($wsdlFile, $options);
|
Chris@14
|
518 $_methods = \array_unique($client->__getFunctions());
|
Chris@14
|
519 unset($client);
|
Chris@14
|
520
|
Chris@14
|
521 \sort($_methods);
|
Chris@14
|
522
|
Chris@14
|
523 $methodTemplate = $this->getTemplate('wsdl_method.tpl');
|
Chris@14
|
524 $methodsBuffer = '';
|
Chris@14
|
525
|
Chris@14
|
526 foreach ($_methods as $method) {
|
Chris@14
|
527 $nameStart = \strpos($method, ' ') + 1;
|
Chris@14
|
528 $nameEnd = \strpos($method, '(');
|
Chris@14
|
529 $name = \substr($method, $nameStart, $nameEnd - $nameStart);
|
Chris@14
|
530
|
Chris@14
|
531 if (empty($methods) || \in_array($name, $methods)) {
|
Chris@14
|
532 $args = \explode(
|
Chris@14
|
533 ',',
|
Chris@14
|
534 \substr(
|
Chris@14
|
535 $method,
|
Chris@14
|
536 $nameEnd + 1,
|
Chris@14
|
537 \strpos($method, ')') - $nameEnd - 1
|
Chris@14
|
538 )
|
Chris@14
|
539 );
|
Chris@14
|
540
|
Chris@14
|
541 foreach (\range(0, \count($args) - 1) as $i) {
|
Chris@14
|
542 $args[$i] = \substr($args[$i], \strpos($args[$i], '$'));
|
Chris@14
|
543 }
|
Chris@14
|
544
|
Chris@14
|
545 $methodTemplate->setVar(
|
Chris@14
|
546 [
|
Chris@14
|
547 'method_name' => $name,
|
Chris@14
|
548 'arguments' => \implode(', ', $args)
|
Chris@14
|
549 ]
|
Chris@14
|
550 );
|
Chris@14
|
551
|
Chris@14
|
552 $methodsBuffer .= $methodTemplate->render();
|
Chris@14
|
553 }
|
Chris@14
|
554 }
|
Chris@14
|
555
|
Chris@14
|
556 $optionsBuffer = 'array(';
|
Chris@14
|
557
|
Chris@14
|
558 foreach ($options as $key => $value) {
|
Chris@14
|
559 $optionsBuffer .= $key . ' => ' . $value;
|
Chris@14
|
560 }
|
Chris@14
|
561
|
Chris@14
|
562 $optionsBuffer .= ')';
|
Chris@14
|
563
|
Chris@14
|
564 $classTemplate = $this->getTemplate('wsdl_class.tpl');
|
Chris@14
|
565 $namespace = '';
|
Chris@14
|
566
|
Chris@14
|
567 if (\strpos($className, '\\') !== false) {
|
Chris@14
|
568 $parts = \explode('\\', $className);
|
Chris@14
|
569 $className = \array_pop($parts);
|
Chris@14
|
570 $namespace = 'namespace ' . \implode('\\', $parts) . ';' . "\n\n";
|
Chris@14
|
571 }
|
Chris@14
|
572
|
Chris@14
|
573 $classTemplate->setVar(
|
Chris@14
|
574 [
|
Chris@14
|
575 'namespace' => $namespace,
|
Chris@14
|
576 'class_name' => $className,
|
Chris@14
|
577 'wsdl' => $wsdlFile,
|
Chris@14
|
578 'options' => $optionsBuffer,
|
Chris@14
|
579 'methods' => $methodsBuffer
|
Chris@14
|
580 ]
|
Chris@14
|
581 );
|
Chris@14
|
582
|
Chris@14
|
583 return $classTemplate->render();
|
Chris@14
|
584 }
|
Chris@14
|
585
|
Chris@14
|
586 /**
|
Chris@14
|
587 * @param array|string $type
|
Chris@14
|
588 * @param array|null $methods
|
Chris@14
|
589 * @param string $mockClassName
|
Chris@14
|
590 * @param bool $callOriginalClone
|
Chris@14
|
591 * @param bool $callAutoload
|
Chris@14
|
592 * @param bool $cloneArguments
|
Chris@14
|
593 * @param bool $callOriginalMethods
|
Chris@14
|
594 *
|
Chris@14
|
595 * @return array
|
Chris@14
|
596 *
|
Chris@14
|
597 * @throws \InvalidArgumentException
|
Chris@14
|
598 * @throws \ReflectionException
|
Chris@14
|
599 * @throws RuntimeException
|
Chris@14
|
600 */
|
Chris@14
|
601 private function generateMock($type, $methods, $mockClassName, $callOriginalClone, $callAutoload, $cloneArguments, $callOriginalMethods)
|
Chris@14
|
602 {
|
Chris@14
|
603 $methodReflections = [];
|
Chris@14
|
604 $classTemplate = $this->getTemplate('mocked_class.tpl');
|
Chris@14
|
605
|
Chris@14
|
606 $additionalInterfaces = [];
|
Chris@14
|
607 $cloneTemplate = '';
|
Chris@14
|
608 $isClass = false;
|
Chris@14
|
609 $isInterface = false;
|
Chris@14
|
610 $isMultipleInterfaces = false;
|
Chris@14
|
611
|
Chris@14
|
612 if (\is_array($type)) {
|
Chris@14
|
613 foreach ($type as $_type) {
|
Chris@14
|
614 if (!\interface_exists($_type, $callAutoload)) {
|
Chris@14
|
615 throw new RuntimeException(
|
Chris@14
|
616 \sprintf(
|
Chris@14
|
617 'Interface "%s" does not exist.',
|
Chris@14
|
618 $_type
|
Chris@14
|
619 )
|
Chris@14
|
620 );
|
Chris@14
|
621 }
|
Chris@14
|
622
|
Chris@14
|
623 $isMultipleInterfaces = true;
|
Chris@14
|
624
|
Chris@14
|
625 $additionalInterfaces[] = $_type;
|
Chris@14
|
626 $typeClass = new ReflectionClass($this->generateClassName(
|
Chris@14
|
627 $_type,
|
Chris@14
|
628 $mockClassName,
|
Chris@14
|
629 'Mock_'
|
Chris@14
|
630 )['fullClassName']
|
Chris@14
|
631 );
|
Chris@14
|
632
|
Chris@14
|
633 foreach ($this->getClassMethods($_type) as $method) {
|
Chris@14
|
634 if (\in_array($method, $methods)) {
|
Chris@14
|
635 throw new RuntimeException(
|
Chris@14
|
636 \sprintf(
|
Chris@14
|
637 'Duplicate method "%s" not allowed.',
|
Chris@14
|
638 $method
|
Chris@14
|
639 )
|
Chris@14
|
640 );
|
Chris@14
|
641 }
|
Chris@14
|
642
|
Chris@14
|
643 $methodReflections[$method] = $typeClass->getMethod($method);
|
Chris@14
|
644 $methods[] = $method;
|
Chris@14
|
645 }
|
Chris@14
|
646 }
|
Chris@14
|
647 }
|
Chris@14
|
648
|
Chris@14
|
649 $mockClassName = $this->generateClassName(
|
Chris@14
|
650 $type,
|
Chris@14
|
651 $mockClassName,
|
Chris@14
|
652 'Mock_'
|
Chris@14
|
653 );
|
Chris@14
|
654
|
Chris@14
|
655 if (\class_exists($mockClassName['fullClassName'], $callAutoload)) {
|
Chris@14
|
656 $isClass = true;
|
Chris@14
|
657 } elseif (\interface_exists($mockClassName['fullClassName'], $callAutoload)) {
|
Chris@14
|
658 $isInterface = true;
|
Chris@14
|
659 }
|
Chris@14
|
660
|
Chris@14
|
661 if (!$isClass && !$isInterface) {
|
Chris@14
|
662 $prologue = 'class ' . $mockClassName['originalClassName'] . "\n{\n}\n\n";
|
Chris@14
|
663
|
Chris@14
|
664 if (!empty($mockClassName['namespaceName'])) {
|
Chris@14
|
665 $prologue = 'namespace ' . $mockClassName['namespaceName'] .
|
Chris@14
|
666 " {\n\n" . $prologue . "}\n\n" .
|
Chris@14
|
667 "namespace {\n\n";
|
Chris@14
|
668
|
Chris@14
|
669 $epilogue = "\n\n}";
|
Chris@14
|
670 }
|
Chris@14
|
671
|
Chris@14
|
672 $cloneTemplate = $this->getTemplate('mocked_clone.tpl');
|
Chris@14
|
673 } else {
|
Chris@14
|
674 $class = new ReflectionClass($mockClassName['fullClassName']);
|
Chris@14
|
675
|
Chris@14
|
676 if ($class->isFinal()) {
|
Chris@14
|
677 throw new RuntimeException(
|
Chris@14
|
678 \sprintf(
|
Chris@14
|
679 'Class "%s" is declared "final" and cannot be mocked.',
|
Chris@14
|
680 $mockClassName['fullClassName']
|
Chris@14
|
681 )
|
Chris@14
|
682 );
|
Chris@14
|
683 }
|
Chris@14
|
684
|
Chris@14
|
685 if ($class->hasMethod('__clone')) {
|
Chris@14
|
686 $cloneMethod = $class->getMethod('__clone');
|
Chris@14
|
687
|
Chris@14
|
688 if (!$cloneMethod->isFinal()) {
|
Chris@14
|
689 if ($callOriginalClone && !$isInterface) {
|
Chris@14
|
690 $cloneTemplate = $this->getTemplate('unmocked_clone.tpl');
|
Chris@14
|
691 } else {
|
Chris@14
|
692 $cloneTemplate = $this->getTemplate('mocked_clone.tpl');
|
Chris@14
|
693 }
|
Chris@14
|
694 }
|
Chris@14
|
695 } else {
|
Chris@14
|
696 $cloneTemplate = $this->getTemplate('mocked_clone.tpl');
|
Chris@14
|
697 }
|
Chris@14
|
698 }
|
Chris@14
|
699
|
Chris@14
|
700 if (\is_object($cloneTemplate)) {
|
Chris@14
|
701 $cloneTemplate = $cloneTemplate->render();
|
Chris@14
|
702 }
|
Chris@14
|
703
|
Chris@14
|
704 if (\is_array($methods) && empty($methods) &&
|
Chris@14
|
705 ($isClass || $isInterface)) {
|
Chris@14
|
706 $methods = $this->getClassMethods($mockClassName['fullClassName']);
|
Chris@14
|
707 }
|
Chris@14
|
708
|
Chris@14
|
709 if (!\is_array($methods)) {
|
Chris@14
|
710 $methods = [];
|
Chris@14
|
711 }
|
Chris@14
|
712
|
Chris@14
|
713 $mockedMethods = '';
|
Chris@14
|
714 $configurable = [];
|
Chris@14
|
715
|
Chris@14
|
716 foreach ($methods as $methodName) {
|
Chris@14
|
717 if ($methodName !== '__construct' && $methodName !== '__clone') {
|
Chris@14
|
718 $configurable[] = \strtolower($methodName);
|
Chris@14
|
719 }
|
Chris@14
|
720 }
|
Chris@14
|
721
|
Chris@14
|
722 if (isset($class)) {
|
Chris@14
|
723 // https://github.com/sebastianbergmann/phpunit-mock-objects/issues/103
|
Chris@14
|
724 if ($isInterface && $class->implementsInterface(Traversable::class) &&
|
Chris@14
|
725 !$class->implementsInterface(Iterator::class) &&
|
Chris@14
|
726 !$class->implementsInterface(IteratorAggregate::class)) {
|
Chris@14
|
727 $additionalInterfaces[] = Iterator::class;
|
Chris@14
|
728 $methods = \array_merge($methods, $this->getClassMethods(Iterator::class));
|
Chris@14
|
729 }
|
Chris@14
|
730
|
Chris@14
|
731 foreach ($methods as $methodName) {
|
Chris@14
|
732 try {
|
Chris@14
|
733 $method = $class->getMethod($methodName);
|
Chris@14
|
734
|
Chris@14
|
735 if ($this->canMockMethod($method)) {
|
Chris@14
|
736 $mockedMethods .= $this->generateMockedMethodDefinitionFromExisting(
|
Chris@14
|
737 $method,
|
Chris@14
|
738 $cloneArguments,
|
Chris@14
|
739 $callOriginalMethods
|
Chris@14
|
740 );
|
Chris@14
|
741 }
|
Chris@14
|
742 } catch (ReflectionException $e) {
|
Chris@14
|
743 $mockedMethods .= $this->generateMockedMethodDefinition(
|
Chris@14
|
744 $mockClassName['fullClassName'],
|
Chris@14
|
745 $methodName,
|
Chris@14
|
746 $cloneArguments
|
Chris@14
|
747 );
|
Chris@14
|
748 }
|
Chris@14
|
749 }
|
Chris@14
|
750 } elseif ($isMultipleInterfaces) {
|
Chris@14
|
751 foreach ($methods as $methodName) {
|
Chris@14
|
752 if ($this->canMockMethod($methodReflections[$methodName])) {
|
Chris@14
|
753 $mockedMethods .= $this->generateMockedMethodDefinitionFromExisting(
|
Chris@14
|
754 $methodReflections[$methodName],
|
Chris@14
|
755 $cloneArguments,
|
Chris@14
|
756 $callOriginalMethods
|
Chris@14
|
757 );
|
Chris@14
|
758 }
|
Chris@14
|
759 }
|
Chris@14
|
760 } else {
|
Chris@14
|
761 foreach ($methods as $methodName) {
|
Chris@14
|
762 $mockedMethods .= $this->generateMockedMethodDefinition(
|
Chris@14
|
763 $mockClassName['fullClassName'],
|
Chris@14
|
764 $methodName,
|
Chris@14
|
765 $cloneArguments
|
Chris@14
|
766 );
|
Chris@14
|
767 }
|
Chris@14
|
768 }
|
Chris@14
|
769
|
Chris@14
|
770 $method = '';
|
Chris@14
|
771
|
Chris@14
|
772 if (!\in_array('method', $methods) && (!isset($class) || !$class->hasMethod('method'))) {
|
Chris@14
|
773 $methodTemplate = $this->getTemplate('mocked_class_method.tpl');
|
Chris@14
|
774
|
Chris@14
|
775 $method = $methodTemplate->render();
|
Chris@14
|
776 }
|
Chris@14
|
777
|
Chris@14
|
778 $classTemplate->setVar(
|
Chris@14
|
779 [
|
Chris@14
|
780 'prologue' => $prologue ?? '',
|
Chris@14
|
781 'epilogue' => $epilogue ?? '',
|
Chris@14
|
782 'class_declaration' => $this->generateMockClassDeclaration(
|
Chris@14
|
783 $mockClassName,
|
Chris@14
|
784 $isInterface,
|
Chris@14
|
785 $additionalInterfaces
|
Chris@14
|
786 ),
|
Chris@14
|
787 'clone' => $cloneTemplate,
|
Chris@14
|
788 'mock_class_name' => $mockClassName['className'],
|
Chris@14
|
789 'mocked_methods' => $mockedMethods,
|
Chris@14
|
790 'method' => $method,
|
Chris@14
|
791 'configurable' => '[' . \implode(', ', \array_map(function ($m) {
|
Chris@14
|
792 return '\'' . $m . '\'';
|
Chris@14
|
793 }, $configurable)) . ']'
|
Chris@14
|
794 ]
|
Chris@14
|
795 );
|
Chris@14
|
796
|
Chris@14
|
797 return [
|
Chris@14
|
798 'code' => $classTemplate->render(),
|
Chris@14
|
799 'mockClassName' => $mockClassName['className']
|
Chris@14
|
800 ];
|
Chris@14
|
801 }
|
Chris@14
|
802
|
Chris@14
|
803 /**
|
Chris@14
|
804 * @param array|string $type
|
Chris@14
|
805 * @param string $className
|
Chris@14
|
806 * @param string $prefix
|
Chris@14
|
807 *
|
Chris@14
|
808 * @return array
|
Chris@14
|
809 */
|
Chris@14
|
810 private function generateClassName($type, $className, $prefix)
|
Chris@14
|
811 {
|
Chris@14
|
812 if (\is_array($type)) {
|
Chris@14
|
813 $type = \implode('_', $type);
|
Chris@14
|
814 }
|
Chris@14
|
815
|
Chris@14
|
816 if ($type[0] === '\\') {
|
Chris@14
|
817 $type = \substr($type, 1);
|
Chris@14
|
818 }
|
Chris@14
|
819
|
Chris@14
|
820 $classNameParts = \explode('\\', $type);
|
Chris@14
|
821
|
Chris@14
|
822 if (\count($classNameParts) > 1) {
|
Chris@14
|
823 $type = \array_pop($classNameParts);
|
Chris@14
|
824 $namespaceName = \implode('\\', $classNameParts);
|
Chris@14
|
825 $fullClassName = $namespaceName . '\\' . $type;
|
Chris@14
|
826 } else {
|
Chris@14
|
827 $namespaceName = '';
|
Chris@14
|
828 $fullClassName = $type;
|
Chris@14
|
829 }
|
Chris@14
|
830
|
Chris@14
|
831 if ($className === '') {
|
Chris@14
|
832 do {
|
Chris@14
|
833 $className = $prefix . $type . '_' .
|
Chris@14
|
834 \substr(\md5(\mt_rand()), 0, 8);
|
Chris@14
|
835 } while (\class_exists($className, false));
|
Chris@14
|
836 }
|
Chris@14
|
837
|
Chris@14
|
838 return [
|
Chris@14
|
839 'className' => $className,
|
Chris@14
|
840 'originalClassName' => $type,
|
Chris@14
|
841 'fullClassName' => $fullClassName,
|
Chris@14
|
842 'namespaceName' => $namespaceName
|
Chris@14
|
843 ];
|
Chris@14
|
844 }
|
Chris@14
|
845
|
Chris@14
|
846 /**
|
Chris@14
|
847 * @param array $mockClassName
|
Chris@14
|
848 * @param bool $isInterface
|
Chris@14
|
849 * @param array $additionalInterfaces
|
Chris@14
|
850 *
|
Chris@14
|
851 * @return string
|
Chris@14
|
852 */
|
Chris@14
|
853 private function generateMockClassDeclaration(array $mockClassName, $isInterface, array $additionalInterfaces = [])
|
Chris@14
|
854 {
|
Chris@14
|
855 $buffer = 'class ';
|
Chris@14
|
856
|
Chris@14
|
857 $additionalInterfaces[] = MockObject::class;
|
Chris@14
|
858 $interfaces = \implode(', ', $additionalInterfaces);
|
Chris@14
|
859
|
Chris@14
|
860 if ($isInterface) {
|
Chris@14
|
861 $buffer .= \sprintf(
|
Chris@14
|
862 '%s implements %s',
|
Chris@14
|
863 $mockClassName['className'],
|
Chris@14
|
864 $interfaces
|
Chris@14
|
865 );
|
Chris@14
|
866
|
Chris@14
|
867 if (!\in_array($mockClassName['originalClassName'], $additionalInterfaces)) {
|
Chris@14
|
868 $buffer .= ', ';
|
Chris@14
|
869
|
Chris@14
|
870 if (!empty($mockClassName['namespaceName'])) {
|
Chris@14
|
871 $buffer .= $mockClassName['namespaceName'] . '\\';
|
Chris@14
|
872 }
|
Chris@14
|
873
|
Chris@14
|
874 $buffer .= $mockClassName['originalClassName'];
|
Chris@14
|
875 }
|
Chris@14
|
876 } else {
|
Chris@14
|
877 $buffer .= \sprintf(
|
Chris@14
|
878 '%s extends %s%s implements %s',
|
Chris@14
|
879 $mockClassName['className'],
|
Chris@14
|
880 !empty($mockClassName['namespaceName']) ? $mockClassName['namespaceName'] . '\\' : '',
|
Chris@14
|
881 $mockClassName['originalClassName'],
|
Chris@14
|
882 $interfaces
|
Chris@14
|
883 );
|
Chris@14
|
884 }
|
Chris@14
|
885
|
Chris@14
|
886 return $buffer;
|
Chris@14
|
887 }
|
Chris@14
|
888
|
Chris@14
|
889 /**
|
Chris@14
|
890 * @param ReflectionMethod $method
|
Chris@14
|
891 * @param bool $cloneArguments
|
Chris@14
|
892 * @param bool $callOriginalMethods
|
Chris@14
|
893 *
|
Chris@14
|
894 * @return string
|
Chris@14
|
895 *
|
Chris@14
|
896 * @throws \PHPUnit\Framework\MockObject\RuntimeException
|
Chris@14
|
897 */
|
Chris@14
|
898 private function generateMockedMethodDefinitionFromExisting(ReflectionMethod $method, $cloneArguments, $callOriginalMethods)
|
Chris@14
|
899 {
|
Chris@14
|
900 if ($method->isPrivate()) {
|
Chris@14
|
901 $modifier = 'private';
|
Chris@14
|
902 } elseif ($method->isProtected()) {
|
Chris@14
|
903 $modifier = 'protected';
|
Chris@14
|
904 } else {
|
Chris@14
|
905 $modifier = 'public';
|
Chris@14
|
906 }
|
Chris@14
|
907
|
Chris@14
|
908 if ($method->isStatic()) {
|
Chris@14
|
909 $modifier .= ' static';
|
Chris@14
|
910 }
|
Chris@14
|
911
|
Chris@14
|
912 if ($method->returnsReference()) {
|
Chris@14
|
913 $reference = '&';
|
Chris@14
|
914 } else {
|
Chris@14
|
915 $reference = '';
|
Chris@14
|
916 }
|
Chris@14
|
917
|
Chris@14
|
918 if ($method->hasReturnType()) {
|
Chris@14
|
919 $returnType = (string) $method->getReturnType();
|
Chris@14
|
920 } else {
|
Chris@14
|
921 $returnType = '';
|
Chris@14
|
922 }
|
Chris@14
|
923
|
Chris@14
|
924 if (\preg_match('#\*[ \t]*+@deprecated[ \t]*+(.*?)\r?+\n[ \t]*+\*(?:[ \t]*+@|/$)#s', $method->getDocComment(), $deprecation)) {
|
Chris@14
|
925 $deprecation = \trim(\preg_replace('#[ \t]*\r?\n[ \t]*+\*[ \t]*+#', ' ', $deprecation[1]));
|
Chris@14
|
926 } else {
|
Chris@14
|
927 $deprecation = false;
|
Chris@14
|
928 }
|
Chris@14
|
929
|
Chris@14
|
930 return $this->generateMockedMethodDefinition(
|
Chris@14
|
931 $method->getDeclaringClass()->getName(),
|
Chris@14
|
932 $method->getName(),
|
Chris@14
|
933 $cloneArguments,
|
Chris@14
|
934 $modifier,
|
Chris@14
|
935 $this->getMethodParameters($method),
|
Chris@14
|
936 $this->getMethodParameters($method, true),
|
Chris@14
|
937 $returnType,
|
Chris@14
|
938 $reference,
|
Chris@14
|
939 $callOriginalMethods,
|
Chris@14
|
940 $method->isStatic(),
|
Chris@14
|
941 $deprecation,
|
Chris@14
|
942 $method->hasReturnType() && PHP_VERSION_ID >= 70100 && $method->getReturnType()->allowsNull()
|
Chris@14
|
943 );
|
Chris@14
|
944 }
|
Chris@14
|
945
|
Chris@14
|
946 /**
|
Chris@14
|
947 * @param string $className
|
Chris@14
|
948 * @param string $methodName
|
Chris@14
|
949 * @param bool $cloneArguments
|
Chris@14
|
950 * @param string $modifier
|
Chris@14
|
951 * @param string $argumentsForDeclaration
|
Chris@14
|
952 * @param string $argumentsForCall
|
Chris@14
|
953 * @param string $returnType
|
Chris@14
|
954 * @param string $reference
|
Chris@14
|
955 * @param bool $callOriginalMethods
|
Chris@14
|
956 * @param bool $static
|
Chris@14
|
957 * @param bool|string $deprecation
|
Chris@14
|
958 * @param bool $allowsReturnNull
|
Chris@14
|
959 *
|
Chris@14
|
960 * @return string
|
Chris@14
|
961 *
|
Chris@14
|
962 * @throws \InvalidArgumentException
|
Chris@14
|
963 */
|
Chris@14
|
964 private function generateMockedMethodDefinition($className, $methodName, $cloneArguments = true, $modifier = 'public', $argumentsForDeclaration = '', $argumentsForCall = '', $returnType = '', $reference = '', $callOriginalMethods = false, $static = false, $deprecation = false, $allowsReturnNull = false)
|
Chris@14
|
965 {
|
Chris@14
|
966 if ($static) {
|
Chris@14
|
967 $templateFile = 'mocked_static_method.tpl';
|
Chris@14
|
968 } else {
|
Chris@14
|
969 if ($returnType === 'void') {
|
Chris@14
|
970 $templateFile = \sprintf(
|
Chris@14
|
971 '%s_method_void.tpl',
|
Chris@14
|
972 $callOriginalMethods ? 'proxied' : 'mocked'
|
Chris@14
|
973 );
|
Chris@14
|
974 } else {
|
Chris@14
|
975 $templateFile = \sprintf(
|
Chris@14
|
976 '%s_method.tpl',
|
Chris@14
|
977 $callOriginalMethods ? 'proxied' : 'mocked'
|
Chris@14
|
978 );
|
Chris@14
|
979 }
|
Chris@14
|
980 }
|
Chris@14
|
981
|
Chris@14
|
982 // Mocked interfaces returning 'self' must explicitly declare the
|
Chris@14
|
983 // interface name as the return type. See
|
Chris@14
|
984 // https://bugs.php.net/bug.php?id=70722
|
Chris@14
|
985 if ($returnType === 'self') {
|
Chris@14
|
986 $returnType = $className;
|
Chris@14
|
987 }
|
Chris@14
|
988
|
Chris@14
|
989 if (false !== $deprecation) {
|
Chris@14
|
990 $deprecation = "The $className::$methodName method is deprecated ($deprecation).";
|
Chris@14
|
991 $deprecationTemplate = $this->getTemplate('deprecation.tpl');
|
Chris@14
|
992
|
Chris@14
|
993 $deprecationTemplate->setVar(
|
Chris@14
|
994 [
|
Chris@14
|
995 'deprecation' => \var_export($deprecation, true),
|
Chris@14
|
996 ]
|
Chris@14
|
997 );
|
Chris@14
|
998
|
Chris@14
|
999 $deprecation = $deprecationTemplate->render();
|
Chris@14
|
1000 }
|
Chris@14
|
1001
|
Chris@14
|
1002 $template = $this->getTemplate($templateFile);
|
Chris@14
|
1003
|
Chris@14
|
1004 $template->setVar(
|
Chris@14
|
1005 [
|
Chris@14
|
1006 'arguments_decl' => $argumentsForDeclaration,
|
Chris@14
|
1007 'arguments_call' => $argumentsForCall,
|
Chris@14
|
1008 'return_delim' => $returnType ? ': ' : '',
|
Chris@14
|
1009 'return_type' => $allowsReturnNull ? '?' . $returnType : $returnType,
|
Chris@14
|
1010 'arguments_count' => !empty($argumentsForCall) ? \substr_count($argumentsForCall, ',') + 1 : 0,
|
Chris@14
|
1011 'class_name' => $className,
|
Chris@14
|
1012 'method_name' => $methodName,
|
Chris@14
|
1013 'modifier' => $modifier,
|
Chris@14
|
1014 'reference' => $reference,
|
Chris@14
|
1015 'clone_arguments' => $cloneArguments ? 'true' : 'false',
|
Chris@14
|
1016 'deprecation' => $deprecation
|
Chris@14
|
1017 ]
|
Chris@14
|
1018 );
|
Chris@14
|
1019
|
Chris@14
|
1020 return $template->render();
|
Chris@14
|
1021 }
|
Chris@14
|
1022
|
Chris@14
|
1023 /**
|
Chris@14
|
1024 * @param ReflectionMethod $method
|
Chris@14
|
1025 *
|
Chris@14
|
1026 * @return bool
|
Chris@14
|
1027 *
|
Chris@14
|
1028 * @throws \ReflectionException
|
Chris@14
|
1029 */
|
Chris@14
|
1030 private function canMockMethod(ReflectionMethod $method)
|
Chris@14
|
1031 {
|
Chris@14
|
1032 return !($method->isConstructor() || $method->isFinal() || $method->isPrivate() || $this->isMethodNameBlacklisted($method->getName()));
|
Chris@14
|
1033 }
|
Chris@14
|
1034
|
Chris@14
|
1035 /**
|
Chris@14
|
1036 * Returns whether a method name is blacklisted
|
Chris@14
|
1037 *
|
Chris@14
|
1038 * @param string $name
|
Chris@14
|
1039 *
|
Chris@14
|
1040 * @return bool
|
Chris@14
|
1041 */
|
Chris@14
|
1042 private function isMethodNameBlacklisted($name)
|
Chris@14
|
1043 {
|
Chris@14
|
1044 return isset($this->blacklistedMethodNames[$name]);
|
Chris@14
|
1045 }
|
Chris@14
|
1046
|
Chris@14
|
1047 /**
|
Chris@14
|
1048 * Returns the parameters of a function or method.
|
Chris@14
|
1049 *
|
Chris@14
|
1050 * @param ReflectionMethod $method
|
Chris@14
|
1051 * @param bool $forCall
|
Chris@14
|
1052 *
|
Chris@14
|
1053 * @return string
|
Chris@14
|
1054 *
|
Chris@14
|
1055 * @throws RuntimeException
|
Chris@14
|
1056 */
|
Chris@14
|
1057 private function getMethodParameters(ReflectionMethod $method, $forCall = false)
|
Chris@14
|
1058 {
|
Chris@14
|
1059 $parameters = [];
|
Chris@14
|
1060
|
Chris@14
|
1061 foreach ($method->getParameters() as $i => $parameter) {
|
Chris@14
|
1062 $name = '$' . $parameter->getName();
|
Chris@14
|
1063
|
Chris@14
|
1064 /* Note: PHP extensions may use empty names for reference arguments
|
Chris@14
|
1065 * or "..." for methods taking a variable number of arguments.
|
Chris@14
|
1066 */
|
Chris@14
|
1067 if ($name === '$' || $name === '$...') {
|
Chris@14
|
1068 $name = '$arg' . $i;
|
Chris@14
|
1069 }
|
Chris@14
|
1070
|
Chris@14
|
1071 if ($parameter->isVariadic()) {
|
Chris@14
|
1072 if ($forCall) {
|
Chris@14
|
1073 continue;
|
Chris@14
|
1074 }
|
Chris@14
|
1075
|
Chris@14
|
1076 $name = '...' . $name;
|
Chris@14
|
1077 }
|
Chris@14
|
1078
|
Chris@14
|
1079 $nullable = '';
|
Chris@14
|
1080 $default = '';
|
Chris@14
|
1081 $reference = '';
|
Chris@14
|
1082 $typeDeclaration = '';
|
Chris@14
|
1083
|
Chris@14
|
1084 if (!$forCall) {
|
Chris@14
|
1085 if (PHP_VERSION_ID >= 70100 && $parameter->hasType() && $parameter->allowsNull()) {
|
Chris@14
|
1086 $nullable = '?';
|
Chris@14
|
1087 }
|
Chris@14
|
1088
|
Chris@14
|
1089 if ($parameter->hasType() && (string) $parameter->getType() !== 'self') {
|
Chris@14
|
1090 $typeDeclaration = (string) $parameter->getType() . ' ';
|
Chris@14
|
1091 } elseif ($parameter->isArray()) {
|
Chris@14
|
1092 $typeDeclaration = 'array ';
|
Chris@14
|
1093 } elseif ($parameter->isCallable()) {
|
Chris@14
|
1094 $typeDeclaration = 'callable ';
|
Chris@14
|
1095 } else {
|
Chris@14
|
1096 try {
|
Chris@14
|
1097 $class = $parameter->getClass();
|
Chris@14
|
1098 } catch (ReflectionException $e) {
|
Chris@14
|
1099 throw new RuntimeException(
|
Chris@14
|
1100 \sprintf(
|
Chris@14
|
1101 'Cannot mock %s::%s() because a class or ' .
|
Chris@14
|
1102 'interface used in the signature is not loaded',
|
Chris@14
|
1103 $method->getDeclaringClass()->getName(),
|
Chris@14
|
1104 $method->getName()
|
Chris@14
|
1105 ),
|
Chris@14
|
1106 0,
|
Chris@14
|
1107 $e
|
Chris@14
|
1108 );
|
Chris@14
|
1109 }
|
Chris@14
|
1110
|
Chris@14
|
1111 if ($class !== null) {
|
Chris@14
|
1112 $typeDeclaration = $class->getName() . ' ';
|
Chris@14
|
1113 }
|
Chris@14
|
1114 }
|
Chris@14
|
1115
|
Chris@14
|
1116 if (!$parameter->isVariadic()) {
|
Chris@14
|
1117 if ($parameter->isDefaultValueAvailable()) {
|
Chris@16
|
1118 $value = $parameter->getDefaultValueConstantName();
|
Chris@16
|
1119
|
Chris@16
|
1120 if ($value === null) {
|
Chris@16
|
1121 $value = \var_export($parameter->getDefaultValue(), true);
|
Chris@17
|
1122 } elseif (!\defined($value)) {
|
Chris@17
|
1123 $rootValue = \preg_replace('/^.*\\\\/', '', $value);
|
Chris@17
|
1124 $value = \defined($rootValue) ? $rootValue : $value;
|
Chris@16
|
1125 }
|
Chris@16
|
1126
|
Chris@16
|
1127 $default = ' = ' . $value;
|
Chris@14
|
1128 } elseif ($parameter->isOptional()) {
|
Chris@14
|
1129 $default = ' = null';
|
Chris@14
|
1130 }
|
Chris@14
|
1131 }
|
Chris@14
|
1132 }
|
Chris@14
|
1133
|
Chris@14
|
1134 if ($parameter->isPassedByReference()) {
|
Chris@14
|
1135 $reference = '&';
|
Chris@14
|
1136 }
|
Chris@14
|
1137
|
Chris@14
|
1138 $parameters[] = $nullable . $typeDeclaration . $reference . $name . $default;
|
Chris@14
|
1139 }
|
Chris@14
|
1140
|
Chris@14
|
1141 return \implode(', ', $parameters);
|
Chris@14
|
1142 }
|
Chris@14
|
1143
|
Chris@14
|
1144 /**
|
Chris@14
|
1145 * @param string $className
|
Chris@14
|
1146 *
|
Chris@14
|
1147 * @return array
|
Chris@14
|
1148 *
|
Chris@14
|
1149 * @throws \ReflectionException
|
Chris@14
|
1150 */
|
Chris@14
|
1151 public function getClassMethods($className)
|
Chris@14
|
1152 {
|
Chris@14
|
1153 $class = new ReflectionClass($className);
|
Chris@14
|
1154 $methods = [];
|
Chris@14
|
1155
|
Chris@14
|
1156 foreach ($class->getMethods() as $method) {
|
Chris@14
|
1157 if ($method->isPublic() || $method->isAbstract()) {
|
Chris@14
|
1158 $methods[] = $method->getName();
|
Chris@14
|
1159 }
|
Chris@14
|
1160 }
|
Chris@14
|
1161
|
Chris@14
|
1162 return $methods;
|
Chris@14
|
1163 }
|
Chris@14
|
1164
|
Chris@14
|
1165 /**
|
Chris@14
|
1166 * @param string $template
|
Chris@14
|
1167 *
|
Chris@14
|
1168 * @return Text_Template
|
Chris@14
|
1169 *
|
Chris@14
|
1170 * @throws \InvalidArgumentException
|
Chris@14
|
1171 */
|
Chris@14
|
1172 private function getTemplate($template)
|
Chris@14
|
1173 {
|
Chris@14
|
1174 $filename = __DIR__ . DIRECTORY_SEPARATOR . 'Generator' . DIRECTORY_SEPARATOR . $template;
|
Chris@14
|
1175
|
Chris@14
|
1176 if (!isset(self::$templates[$filename])) {
|
Chris@14
|
1177 self::$templates[$filename] = new Text_Template($filename);
|
Chris@14
|
1178 }
|
Chris@14
|
1179
|
Chris@14
|
1180 return self::$templates[$filename];
|
Chris@14
|
1181 }
|
Chris@14
|
1182 }
|