comparison vendor/phpunit/phpunit-mock-objects/src/Generator.php @ 14:1fec387a4317

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