Passed
Pull Request — master (#1)
by Guillaume
04:03
created

Generator::generateMock()   F

Complexity

Conditions 41
Paths 7152

Size

Total Lines 232
Code Lines 140

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 41
eloc 140
nc 7152
nop 7
dl 0
loc 232
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types=1);
2
/*
3
 * This file is part of PHPUnit.
4
 *
5
 * (c) Sebastian Bergmann <[email protected]>
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 PHPUnit\Framework\InvalidArgumentException;
15
use SebastianBergmann\Template\Template;
16
17
/**
18
 * @internal This class is not covered by the backward compatibility promise for PHPUnit
19
 */
20
final class Generator
21
{
22
    /**
23
     * @var array
24
     */
25
    private const BLACKLISTED_METHOD_NAMES = [
26
        '__CLASS__'       => true,
27
        '__DIR__'         => true,
28
        '__FILE__'        => true,
29
        '__FUNCTION__'    => true,
30
        '__LINE__'        => true,
31
        '__METHOD__'      => true,
32
        '__NAMESPACE__'   => true,
33
        '__TRAIT__'       => true,
34
        '__clone'         => true,
35
        '__halt_compiler' => true,
36
    ];
37
38
    /**
39
     * @var array
40
     */
41
    private static $cache = [];
42
43
    /**
44
     * @var Template[]
45
     */
46
    private static $templates = [];
47
48
    /**
49
     * Returns a mock object for the specified class.
50
     *
51
     * @param null|array $methods
52
     *
53
     * @throws RuntimeException
54
     */
55
    public function getMock(string $type, $methods = [], array $arguments = [], string $mockClassName = '', bool $callOriginalConstructor = true, bool $callOriginalClone = true, bool $callAutoload = true, bool $cloneArguments = true, bool $callOriginalMethods = false, object $proxyTarget = null, bool $allowMockingUnknownTypes = true, bool $returnValueGeneration = true): MockObject
56
    {
57
        if (!\is_array($methods) && null !== $methods) {
58
            throw InvalidArgumentException::create(2, 'array');
59
        }
60
61
        if ($type === 'Traversable' || $type === '\\Traversable') {
62
            $type = 'Iterator';
63
        }
64
65
        if (!$allowMockingUnknownTypes && !\class_exists($type, $callAutoload) && !\interface_exists($type, $callAutoload)) {
66
            throw new RuntimeException(
67
                \sprintf(
68
                    'Cannot stub or mock class or interface "%s" which does not exist',
69
                    $type
70
                )
71
            );
72
        }
73
74
        if (null !== $methods) {
75
            foreach ($methods as $method) {
76
                if (!\preg_match('~[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*~', (string) $method)) {
77
                    throw new RuntimeException(
78
                        \sprintf(
79
                            'Cannot stub or mock method with invalid name "%s"',
80
                            $method
81
                        )
82
                    );
83
                }
84
            }
85
86
            if ($methods !== \array_unique($methods)) {
87
                throw new RuntimeException(
88
                    \sprintf(
89
                        'Cannot stub or mock using a method list that contains duplicates: "%s" (duplicate: "%s")',
90
                        \implode(', ', $methods),
91
                        \implode(', ', \array_unique(\array_diff_assoc($methods, \array_unique($methods))))
92
                    )
93
                );
94
            }
95
        }
96
97
        if ($mockClassName !== '' && \class_exists($mockClassName, false)) {
98
            try {
99
                $reflector = new \ReflectionClass($mockClassName);
100
                // @codeCoverageIgnoreStart
101
            } catch (\ReflectionException $e) {
102
                throw new RuntimeException(
103
                    $e->getMessage(),
104
                    (int) $e->getCode(),
105
                    $e
106
                );
107
            }
108
            // @codeCoverageIgnoreEnd
109
110
            if (!$reflector->implementsInterface(MockObject::class)) {
111
                throw new RuntimeException(
112
                    \sprintf(
113
                        'Class "%s" already exists.',
114
                        $mockClassName
115
                    )
116
                );
117
            }
118
        }
119
120
        if (!$callOriginalConstructor && $callOriginalMethods) {
121
            throw new RuntimeException(
122
                'Proxying to original methods requires invoking the original constructor'
123
            );
124
        }
125
126
        $mock = $this->generate(
127
            $type,
128
            $methods,
129
            $mockClassName,
130
            $callOriginalClone,
131
            $callAutoload,
132
            $cloneArguments,
133
            $callOriginalMethods
134
        );
135
136
        return $this->getObject(
137
            $mock,
138
            $type,
139
            $callOriginalConstructor,
140
            $callAutoload,
141
            $arguments,
142
            $callOriginalMethods,
143
            $proxyTarget,
144
            $returnValueGeneration
145
        );
146
    }
147
148
    /**
149
     * Returns a mock object for the specified abstract class with all abstract
150
     * methods of the class mocked. Concrete methods to mock can be specified with
151
     * the $mockedMethods parameter
152
     *
153
     * @psalm-template RealInstanceType of object
154
     * @psalm-param class-string<RealInstanceType> $originalClassName
155
     * @psalm-return MockObject&RealInstanceType
156
     *
157
     * @throws RuntimeException
158
     */
159
    public function getMockForAbstractClass(string $originalClassName, array $arguments = [], string $mockClassName = '', bool $callOriginalConstructor = true, bool $callOriginalClone = true, bool $callAutoload = true, array $mockedMethods = null, bool $cloneArguments = true): MockObject
160
    {
161
        if (\class_exists($originalClassName, $callAutoload) ||
162
            \interface_exists($originalClassName, $callAutoload)) {
163
            try {
164
                $reflector = new \ReflectionClass($originalClassName);
165
                // @codeCoverageIgnoreStart
166
            } catch (\ReflectionException $e) {
167
                throw new RuntimeException(
168
                    $e->getMessage(),
169
                    (int) $e->getCode(),
170
                    $e
171
                );
172
            }
173
            // @codeCoverageIgnoreEnd
174
175
            $methods = $mockedMethods;
176
177
            foreach ($reflector->getMethods() as $method) {
178
                if ($method->isAbstract() && !\in_array($method->getName(), $methods ?? [], true)) {
179
                    $methods[] = $method->getName();
180
                }
181
            }
182
183
            if (empty($methods)) {
184
                $methods = null;
185
            }
186
187
            return $this->getMock(
188
                $originalClassName,
189
                $methods,
190
                $arguments,
191
                $mockClassName,
192
                $callOriginalConstructor,
193
                $callOriginalClone,
194
                $callAutoload,
195
                $cloneArguments
196
            );
197
        }
198
199
        throw new RuntimeException(
200
            \sprintf('Class "%s" does not exist.', $originalClassName)
201
        );
202
    }
203
204
    /**
205
     * Returns a mock object for the specified trait with all abstract methods
206
     * of the trait mocked. Concrete methods to mock can be specified with the
207
     * `$mockedMethods` parameter.
208
     *
209
     * @throws RuntimeException
210
     */
211
    public function getMockForTrait(string $traitName, array $arguments = [], string $mockClassName = '', bool $callOriginalConstructor = true, bool $callOriginalClone = true, bool $callAutoload = true, array $mockedMethods = null, bool $cloneArguments = true): MockObject
212
    {
213
        if (!\trait_exists($traitName, $callAutoload)) {
214
            throw new RuntimeException(
215
                \sprintf(
216
                    'Trait "%s" does not exist.',
217
                    $traitName
218
                )
219
            );
220
        }
221
222
        $className = $this->generateClassName(
223
            $traitName,
224
            '',
225
            'Trait_'
226
        );
227
228
        $classTemplate = $this->getTemplate('trait_class.tpl');
229
230
        $classTemplate->setVar(
231
            [
232
                'prologue'   => 'abstract ',
233
                'class_name' => $className['className'],
234
                'trait_name' => $traitName,
235
            ]
236
        );
237
238
        $mockTrait = new MockTrait($classTemplate->render(), $className['className']);
239
        $mockTrait->generate();
240
241
        return $this->getMockForAbstractClass($className['className'], $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $mockedMethods, $cloneArguments);
242
    }
243
244
    /**
245
     * Returns an object for the specified trait.
246
     *
247
     * @throws RuntimeException
248
     */
249
    public function getObjectForTrait(string $traitName, string $traitClassName = '', bool $callAutoload = true, bool $callOriginalConstructor = false, array $arguments = []): object
250
    {
251
        if (!\trait_exists($traitName, $callAutoload)) {
252
            throw new RuntimeException(
253
                \sprintf(
254
                    'Trait "%s" does not exist.',
255
                    $traitName
256
                )
257
            );
258
        }
259
260
        $className = $this->generateClassName(
261
            $traitName,
262
            $traitClassName,
263
            'Trait_'
264
        );
265
266
        $classTemplate = $this->getTemplate('trait_class.tpl');
267
268
        $classTemplate->setVar(
269
            [
270
                'prologue'   => '',
271
                'class_name' => $className['className'],
272
                'trait_name' => $traitName,
273
            ]
274
        );
275
276
        return $this->getObject(
277
            new MockTrait(
278
                $classTemplate->render(),
279
                $className['className']
280
            ),
281
            '',
282
            $callOriginalConstructor,
283
            $callAutoload,
284
            $arguments
285
        );
286
    }
287
288
    public function generate(string $type, array $methods = null, string $mockClassName = '', bool $callOriginalClone = true, bool $callAutoload = true, bool $cloneArguments = true, bool $callOriginalMethods = false): MockClass
289
    {
290
        if ($mockClassName !== '') {
291
            return $this->generateMock(
292
                $type,
293
                $methods,
294
                $mockClassName,
295
                $callOriginalClone,
296
                $callAutoload,
297
                $cloneArguments,
298
                $callOriginalMethods
299
            );
300
        }
301
302
        $key = \md5(
303
            $type .
304
            \serialize($methods) .
305
            \serialize($callOriginalClone) .
306
            \serialize($cloneArguments) .
307
            \serialize($callOriginalMethods)
308
        );
309
310
        if (!isset(self::$cache[$key])) {
311
            self::$cache[$key] = $this->generateMock(
312
                $type,
313
                $methods,
314
                $mockClassName,
315
                $callOriginalClone,
316
                $callAutoload,
317
                $cloneArguments,
318
                $callOriginalMethods
319
            );
320
        }
321
322
        return self::$cache[$key];
323
    }
324
325
    /**
326
     * @throws RuntimeException
327
     */
328
    public function generateClassFromWsdl(string $wsdlFile, string $className, array $methods = [], array $options = []): string
329
    {
330
        if (!\extension_loaded('soap')) {
331
            throw new RuntimeException(
332
                'The SOAP extension is required to generate a mock object from WSDL.'
333
            );
334
        }
335
336
        $options = \array_merge($options, ['cache_wsdl' => \WSDL_CACHE_NONE]);
337
338
        try {
339
            $client   = new \SoapClient($wsdlFile, $options);
340
            $_methods = \array_unique($client->__getFunctions());
341
            unset($client);
342
        } catch (\SoapFault $e) {
343
            throw new RuntimeException(
344
                $e->getMessage(),
345
                (int) $e->getCode(),
346
                $e
347
            );
348
        }
349
350
        \sort($_methods);
351
352
        $methodTemplate = $this->getTemplate('wsdl_method.tpl');
353
        $methodsBuffer  = '';
354
355
        foreach ($_methods as $method) {
356
            \preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*\(/', $method, $matches, \PREG_OFFSET_CAPTURE);
357
            $lastFunction = \array_pop($matches[0]);
358
            $nameStart    = $lastFunction[1];
359
            $nameEnd      = $nameStart + \strlen($lastFunction[0]) - 1;
360
            $name         = \str_replace('(', '', $lastFunction[0]);
361
362
            if (empty($methods) || \in_array($name, $methods, true)) {
363
                $args = \explode(
364
                    ',',
365
                    \str_replace(')', '', \substr($method, $nameEnd + 1))
366
                );
367
368
                foreach (\range(0, \count($args) - 1) as $i) {
369
                    $args[$i] = \substr($args[$i], \strpos($args[$i], '$'));
370
                }
371
372
                $methodTemplate->setVar(
373
                    [
374
                        'method_name' => $name,
375
                        'arguments'   => \implode(', ', $args),
376
                    ]
377
                );
378
379
                $methodsBuffer .= $methodTemplate->render();
380
            }
381
        }
382
383
        $optionsBuffer = '[';
384
385
        foreach ($options as $key => $value) {
386
            $optionsBuffer .= $key . ' => ' . $value;
387
        }
388
389
        $optionsBuffer .= ']';
390
391
        $classTemplate = $this->getTemplate('wsdl_class.tpl');
392
        $namespace     = '';
393
394
        if (\strpos($className, '\\') !== false) {
395
            $parts     = \explode('\\', $className);
396
            $className = \array_pop($parts);
397
            $namespace = 'namespace ' . \implode('\\', $parts) . ';' . "\n\n";
398
        }
399
400
        $classTemplate->setVar(
401
            [
402
                'namespace'  => $namespace,
403
                'class_name' => $className,
404
                'wsdl'       => $wsdlFile,
405
                'options'    => $optionsBuffer,
406
                'methods'    => $methodsBuffer,
407
            ]
408
        );
409
410
        return $classTemplate->render();
411
    }
412
413
    /**
414
     * @throws RuntimeException
415
     *
416
     * @return string[]
417
     */
418
    public function getClassMethods(string $className): array
419
    {
420
        try {
421
            $class = new \ReflectionClass($className);
422
            // @codeCoverageIgnoreStart
423
        } catch (\ReflectionException $e) {
424
            throw new RuntimeException(
425
                $e->getMessage(),
426
                (int) $e->getCode(),
427
                $e
428
            );
429
        }
430
        // @codeCoverageIgnoreEnd
431
432
        $methods = [];
433
434
        foreach ($class->getMethods() as $method) {
435
            if ($method->isPublic() || $method->isAbstract()) {
436
                $methods[] = $method->getName();
437
            }
438
        }
439
440
        return $methods;
441
    }
442
443
    /**
444
     * @throws RuntimeException
445
     *
446
     * @return MockMethod[]
447
     */
448
    public function mockClassMethods(string $className, bool $callOriginalMethods, bool $cloneArguments): array
449
    {
450
        try {
451
            $class = new \ReflectionClass($className);
452
            // @codeCoverageIgnoreStart
453
        } catch (\ReflectionException $e) {
454
            throw new RuntimeException(
455
                $e->getMessage(),
456
                (int) $e->getCode(),
457
                $e
458
            );
459
        }
460
        // @codeCoverageIgnoreEnd
461
462
        $methods = [];
463
464
        foreach ($class->getMethods() as $method) {
465
            if (($method->isPublic() || $method->isAbstract()) && $this->canMockMethod($method)) {
466
                $methods[] = MockMethod::fromReflection($method, $callOriginalMethods, $cloneArguments);
467
            }
468
        }
469
470
        return $methods;
471
    }
472
473
    /**
474
     * @throws RuntimeException
475
     *
476
     * @return MockMethod[]
477
     */
478
    public function mockInterfaceMethods(string $interfaceName, bool $cloneArguments): array
479
    {
480
        try {
481
            $class = new \ReflectionClass($interfaceName);
482
            // @codeCoverageIgnoreStart
483
        } catch (\ReflectionException $e) {
484
            throw new RuntimeException(
485
                $e->getMessage(),
486
                (int) $e->getCode(),
487
                $e
488
            );
489
        }
490
        // @codeCoverageIgnoreEnd
491
492
        $methods = [];
493
494
        foreach ($class->getMethods() as $method) {
495
            $methods[] = MockMethod::fromReflection($method, false, $cloneArguments);
496
        }
497
498
        return $methods;
499
    }
500
501
    /**
502
     * @psalm-param class-string $interfaceName
503
     *
504
     * @return \ReflectionMethod[]
505
     */
506
    private function userDefinedInterfaceMethods(string $interfaceName): array
507
    {
508
        try {
509
            // @codeCoverageIgnoreStart
510
            $interface = new \ReflectionClass($interfaceName);
511
        } catch (\ReflectionException $e) {
512
            throw new RuntimeException(
513
                $e->getMessage(),
514
                (int) $e->getCode(),
515
                $e
516
            );
517
        }
518
        // @codeCoverageIgnoreEnd
519
520
        $methods = [];
521
522
        foreach ($interface->getMethods() as $method) {
523
            if (!$method->isUserDefined()) {
524
                continue;
525
            }
526
527
            $methods[] = $method;
528
        }
529
530
        return $methods;
531
    }
532
533
    private function getObject(MockType $mockClass, $type = '', bool $callOriginalConstructor = false, bool $callAutoload = false, array $arguments = [], bool $callOriginalMethods = false, object $proxyTarget = null, bool $returnValueGeneration = true)
534
    {
535
        $className = $mockClass->generate();
536
537
        if ($callOriginalConstructor) {
538
            if (\count($arguments) === 0) {
539
                $object = new $className;
540
            } else {
541
                try {
542
                    $class = new \ReflectionClass($className);
543
                    // @codeCoverageIgnoreStart
544
                } catch (\ReflectionException $e) {
545
                    throw new RuntimeException(
546
                        $e->getMessage(),
547
                        (int) $e->getCode(),
548
                        $e
549
                    );
550
                }
551
                // @codeCoverageIgnoreEnd
552
553
                $object = $class->newInstanceArgs($arguments);
554
            }
555
        } else {
556
            try {
557
                $object = (new Instantiator)->instantiate($className);
558
            } catch (InstantiatorException $exception) {
559
                throw new RuntimeException($exception->getMessage());
560
            }
561
        }
562
563
        if ($callOriginalMethods) {
564
            if (!\is_object($proxyTarget)) {
565
                if (\count($arguments) === 0) {
566
                    $proxyTarget = new $type;
567
                } else {
568
                    try {
569
                        $class = new \ReflectionClass($type);
570
                        // @codeCoverageIgnoreStart
571
                    } catch (\ReflectionException $e) {
572
                        throw new RuntimeException(
573
                            $e->getMessage(),
574
                            (int) $e->getCode(),
575
                            $e
576
                        );
577
                    }
578
                    // @codeCoverageIgnoreEnd
579
580
                    $proxyTarget = $class->newInstanceArgs($arguments);
581
                }
582
            }
583
584
            $object->__phpunit_setOriginalObject($proxyTarget);
585
        }
586
587
        if ($object instanceof MockObject) {
588
            $object->__phpunit_setReturnValueGeneration($returnValueGeneration);
589
        }
590
591
        return $object;
592
    }
593
594
    /**
595
     * @throws RuntimeException
596
     */
597
    private function generateMock(string $type, ?array $explicitMethods, string $mockClassName, bool $callOriginalClone, bool $callAutoload, bool $cloneArguments, bool $callOriginalMethods): MockClass
598
    {
599
        $classTemplate        = $this->getTemplate('mocked_class.tpl');
600
        $additionalInterfaces = [];
601
        $mockedCloneMethod    = false;
602
        $unmockedCloneMethod  = false;
603
        $isClass              = false;
604
        $isInterface          = false;
605
        $class                = null;
606
        $mockMethods          = new MockMethodSet;
607
608
        $_mockClassName = $this->generateClassName(
609
            $type,
610
            $mockClassName,
611
            'Mock_'
612
        );
613
614
        if (\class_exists($_mockClassName['fullClassName'], $callAutoload)) {
615
            $isClass = true;
616
        } elseif (\interface_exists($_mockClassName['fullClassName'], $callAutoload)) {
617
            $isInterface = true;
618
        }
619
620
        if (!$isClass && !$isInterface) {
621
            $prologue = 'class ' . $_mockClassName['originalClassName'] . "\n{\n}\n\n";
622
623
            if (!empty($_mockClassName['namespaceName'])) {
624
                $prologue = 'namespace ' . $_mockClassName['namespaceName'] .
625
                            " {\n\n" . $prologue . "}\n\n" .
626
                            "namespace {\n\n";
627
628
                $epilogue = "\n\n}";
629
            }
630
631
            $mockedCloneMethod = true;
632
        } else {
633
            try {
634
                $class = new \ReflectionClass($_mockClassName['fullClassName']);
635
                // @codeCoverageIgnoreStart
636
            } catch (\ReflectionException $e) {
637
                throw new RuntimeException(
638
                    $e->getMessage(),
639
                    (int) $e->getCode(),
640
                    $e
641
                );
642
            }
643
            // @codeCoverageIgnoreEnd
644
645
            if ($class->isFinal()) {
646
                throw new RuntimeException(
647
                    \sprintf(
648
                        'Class "%s" is declared "final" and cannot be mocked.',
649
                        $_mockClassName['fullClassName']
650
                    )
651
                );
652
            }
653
654
            // @see https://github.com/sebastianbergmann/phpunit/issues/2995
655
            if ($isInterface && $class->implementsInterface(\Throwable::class)) {
656
                $actualClassName        = \Exception::class;
657
                $additionalInterfaces[] = $class->getName();
658
                $isInterface            = false;
659
660
                try {
661
                    $class = new \ReflectionClass($actualClassName);
662
                    // @codeCoverageIgnoreStart
663
                } catch (\ReflectionException $e) {
664
                    throw new RuntimeException(
665
                        $e->getMessage(),
666
                        (int) $e->getCode(),
667
                        $e
668
                    );
669
                }
670
                // @codeCoverageIgnoreEnd
671
672
                foreach ($this->userDefinedInterfaceMethods($_mockClassName['fullClassName']) as $method) {
673
                    $methodName = $method->getName();
674
675
                    if ($class->hasMethod($methodName)) {
676
                        try {
677
                            $classMethod = $class->getMethod($methodName);
678
                            // @codeCoverageIgnoreStart
679
                        } catch (\ReflectionException $e) {
680
                            throw new RuntimeException(
681
                                $e->getMessage(),
682
                                (int) $e->getCode(),
683
                                $e
684
                            );
685
                        }
686
                        // @codeCoverageIgnoreEnd
687
688
                        if (!$this->canMockMethod($classMethod)) {
689
                            continue;
690
                        }
691
                    }
692
693
                    $mockMethods->addMethods(
694
                        MockMethod::fromReflection($method, $callOriginalMethods, $cloneArguments)
695
                    );
696
                }
697
698
                $_mockClassName = $this->generateClassName(
699
                    $actualClassName,
700
                    $_mockClassName['className'],
701
                    'Mock_'
702
                );
703
            }
704
705
            // @see https://github.com/sebastianbergmann/phpunit-mock-objects/issues/103
706
            if ($isInterface && $class->implementsInterface(\Traversable::class) &&
707
                !$class->implementsInterface(\Iterator::class) &&
708
                !$class->implementsInterface(\IteratorAggregate::class)) {
709
                $additionalInterfaces[] = \Iterator::class;
710
711
                $mockMethods->addMethods(
712
                    ...$this->mockClassMethods(\Iterator::class, $callOriginalMethods, $cloneArguments)
713
                );
714
            }
715
716
            if ($class->hasMethod('__clone')) {
717
                try {
718
                    $cloneMethod = $class->getMethod('__clone');
719
                    // @codeCoverageIgnoreStart
720
                } catch (\ReflectionException $e) {
721
                    throw new RuntimeException(
722
                        $e->getMessage(),
723
                        (int) $e->getCode(),
724
                        $e
725
                    );
726
                }
727
                // @codeCoverageIgnoreEnd
728
729
                if (!$cloneMethod->isFinal()) {
730
                    if ($callOriginalClone && !$isInterface) {
731
                        $unmockedCloneMethod = true;
732
                    } else {
733
                        $mockedCloneMethod = true;
734
                    }
735
                }
736
            } else {
737
                $mockedCloneMethod = true;
738
            }
739
        }
740
741
        if ($isClass && $explicitMethods === []) {
742
            $mockMethods->addMethods(
743
                ...$this->mockClassMethods($_mockClassName['fullClassName'], $callOriginalMethods, $cloneArguments)
744
            );
745
        }
746
747
        if ($isInterface && ($explicitMethods === [] || $explicitMethods === null)) {
748
            $mockMethods->addMethods(
749
                ...$this->mockInterfaceMethods($_mockClassName['fullClassName'], $cloneArguments)
750
            );
751
        }
752
753
        if (\is_array($explicitMethods)) {
754
            foreach ($explicitMethods as $methodName) {
755
                if ($class !== null && $class->hasMethod($methodName)) {
756
                    try {
757
                        $method = $class->getMethod($methodName);
758
                        // @codeCoverageIgnoreStart
759
                    } catch (\ReflectionException $e) {
760
                        throw new RuntimeException(
761
                            $e->getMessage(),
762
                            (int) $e->getCode(),
763
                            $e
764
                        );
765
                    }
766
                    // @codeCoverageIgnoreEnd
767
768
                    if ($this->canMockMethod($method)) {
769
                        $mockMethods->addMethods(
770
                            MockMethod::fromReflection($method, $callOriginalMethods, $cloneArguments)
771
                        );
772
                    }
773
                } else {
774
                    $mockMethods->addMethods(
775
                        MockMethod::fromName(
776
                            $_mockClassName['fullClassName'],
777
                            $methodName,
778
                            $cloneArguments
779
                        )
780
                    );
781
                }
782
            }
783
        }
784
785
        $mockedMethods = '';
786
        $configurable  = [];
787
788
        foreach ($mockMethods->asArray() as $mockMethod) {
789
            $mockedMethods .= $mockMethod->generateCode();
790
            $configurable[] = new ConfigurableMethod($mockMethod->getName(), $mockMethod->getReturnType());
791
        }
792
793
        $method = '';
794
795
        if (!$mockMethods->hasMethod('method') && (!isset($class) || !$class->hasMethod('method'))) {
796
            $method = \PHP_EOL . '    use \PHPUnit\Framework\MockObject\Method;';
797
        }
798
799
        $cloneTrait = '';
800
801
        if ($mockedCloneMethod) {
802
            $cloneTrait = \PHP_EOL . '    use \PHPUnit\Framework\MockObject\MockedCloneMethod;';
803
        }
804
805
        if ($unmockedCloneMethod) {
806
            $cloneTrait = \PHP_EOL . '    use \PHPUnit\Framework\MockObject\UnmockedCloneMethod;';
807
        }
808
809
        $classTemplate->setVar(
810
            [
811
                'prologue'          => $prologue ?? '',
812
                'epilogue'          => $epilogue ?? '',
813
                'class_declaration' => $this->generateMockClassDeclaration(
814
                    $_mockClassName,
815
                    $isInterface,
816
                    $additionalInterfaces
817
                ),
818
                'clone'           => $cloneTrait,
819
                'mock_class_name' => $_mockClassName['className'],
820
                'mocked_methods'  => $mockedMethods,
821
                'method'          => $method,
822
            ]
823
        );
824
825
        return new MockClass(
826
            $classTemplate->render(),
827
            $_mockClassName['className'],
828
            $configurable
829
        );
830
    }
831
832
    private function generateClassName(string $type, string $className, string $prefix): array
833
    {
834
        if ($type[0] === '\\') {
835
            $type = \substr($type, 1);
836
        }
837
838
        $classNameParts = \explode('\\', $type);
839
840
        if (\count($classNameParts) > 1) {
841
            $type          = \array_pop($classNameParts);
842
            $namespaceName = \implode('\\', $classNameParts);
843
            $fullClassName = $namespaceName . '\\' . $type;
844
        } else {
845
            $namespaceName = '';
846
            $fullClassName = $type;
847
        }
848
849
        if ($className === '') {
850
            do {
851
                $className = $prefix . $type . '_' .
852
                             \substr(\md5((string) \mt_rand()), 0, 8);
853
            } while (\class_exists($className, false));
854
        }
855
856
        return [
857
            'className'         => $className,
858
            'originalClassName' => $type,
859
            'fullClassName'     => $fullClassName,
860
            'namespaceName'     => $namespaceName,
861
        ];
862
    }
863
864
    private function generateMockClassDeclaration(array $mockClassName, bool $isInterface, array $additionalInterfaces = []): string
865
    {
866
        $buffer = 'class ';
867
868
        $additionalInterfaces[] = MockObject::class;
869
        $interfaces             = \implode(', ', $additionalInterfaces);
870
871
        if ($isInterface) {
872
            $buffer .= \sprintf(
873
                '%s implements %s',
874
                $mockClassName['className'],
875
                $interfaces
876
            );
877
878
            if (!\in_array($mockClassName['originalClassName'], $additionalInterfaces, true)) {
879
                $buffer .= ', ';
880
881
                if (!empty($mockClassName['namespaceName'])) {
882
                    $buffer .= $mockClassName['namespaceName'] . '\\';
883
                }
884
885
                $buffer .= $mockClassName['originalClassName'];
886
            }
887
        } else {
888
            $buffer .= \sprintf(
889
                '%s extends %s%s implements %s',
890
                $mockClassName['className'],
891
                !empty($mockClassName['namespaceName']) ? $mockClassName['namespaceName'] . '\\' : '',
892
                $mockClassName['originalClassName'],
893
                $interfaces
894
            );
895
        }
896
897
        return $buffer;
898
    }
899
900
    private function canMockMethod(\ReflectionMethod $method): bool
901
    {
902
        return !($this->isConstructor($method) || $method->isFinal() || $method->isPrivate() || $this->isMethodNameBlacklisted($method->getName()));
903
    }
904
905
    private function isMethodNameBlacklisted(string $name): bool
906
    {
907
        return isset(self::BLACKLISTED_METHOD_NAMES[$name]);
908
    }
909
910
    private function getTemplate(string $template): Template
911
    {
912
        $filename = __DIR__ . \DIRECTORY_SEPARATOR . 'Generator' . \DIRECTORY_SEPARATOR . $template;
913
914
        if (!isset(self::$templates[$filename])) {
915
            self::$templates[$filename] = new Template($filename);
916
        }
917
918
        return self::$templates[$filename];
919
    }
920
921
    /**
922
     * @see https://github.com/sebastianbergmann/phpunit/issues/4139#issuecomment-605409765
923
     */
924
    private function isConstructor(\ReflectionMethod $method): bool
925
    {
926
        $methodName = \strtolower($method->getName());
927
928
        if ($methodName === '__construct') {
929
            return true;
930
        }
931
932
        if (\PHP_MAJOR_VERSION >= 8) {
933
            return false;
934
        }
935
936
        $className = \strtolower($method->getDeclaringClass()->getName());
937
938
        return $methodName === $className;
939
    }
940
}
941