Subversion-Projekte lars-tiefland.laravel_shop

Revision

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