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