Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 150 | Zur aktuellen Revision | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

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