Generator::getMock()   F
last analyzed

Complexity

Conditions 28
Paths 155

Size

Total Lines 124
Code Lines 75

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 28
eloc 75
nc 155
nop 12
dl 0
loc 124
rs 3.7083
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
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 Iterator;
15
use IteratorAggregate;
16
use PHPUnit\Framework\Exception;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, PHPUnit\Framework\MockObject\Exception. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
17
use PHPUnit\Util\InvalidArgumentHelper;
18
use ReflectionClass;
19
use ReflectionMethod;
20
use SoapClient;
21
use Text_Template;
22
use Traversable;
23
24
/**
25
 * Mock Object Code Generator
26
 */
27
class Generator
28
{
29
    /**
30
     * @var array
31
     */
32
    private const BLACKLISTED_METHOD_NAMES = [
33
        '__CLASS__'       => true,
34
        '__DIR__'         => true,
35
        '__FILE__'        => true,
36
        '__FUNCTION__'    => true,
37
        '__LINE__'        => true,
38
        '__METHOD__'      => true,
39
        '__NAMESPACE__'   => true,
40
        '__TRAIT__'       => true,
41
        '__clone'         => true,
42
        '__halt_compiler' => true,
43
    ];
44
45
    /**
46
     * @var array
47
     */
48
    private static $cache = [];
49
50
    /**
51
     * @var Text_Template[]
52
     */
53
    private static $templates = [];
54
55
    /**
56
     * Returns a mock object for the specified class.
57
     *
58
     * @param string|string[] $type
59
     * @param array           $methods
60
     * @param string          $mockClassName
61
     * @param bool            $callOriginalConstructor
62
     * @param bool            $callOriginalClone
63
     * @param bool            $callAutoload
64
     * @param bool            $cloneArguments
65
     * @param bool            $callOriginalMethods
66
     * @param object          $proxyTarget
67
     * @param bool            $allowMockingUnknownTypes
68
     * @param bool            $returnValueGeneration
69
     *
70
     * @throws Exception
71
     * @throws RuntimeException
72
     * @throws \PHPUnit\Framework\Exception
73
     * @throws \ReflectionException
74
     *
75
     * @return MockObject
76
     */
77
    public function getMock($type, $methods = [], array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $cloneArguments = true, $callOriginalMethods = false, $proxyTarget = null, $allowMockingUnknownTypes = true, $returnValueGeneration = true)
78
    {
79
        if (!\is_array($type) && !\is_string($type)) {
0 ignored issues
show
introduced by
The condition is_string($type) is always true.
Loading history...
80
            throw InvalidArgumentHelper::factory(1, 'array or string');
81
        }
82
83
        if (!\is_string($mockClassName)) {
0 ignored issues
show
introduced by
The condition is_string($mockClassName) is always true.
Loading history...
84
            throw InvalidArgumentHelper::factory(4, 'string');
85
        }
86
87
        if (!\is_array($methods) && null !== $methods) {
0 ignored issues
show
introduced by
The condition is_array($methods) is always true.
Loading history...
88
            throw InvalidArgumentHelper::factory(2, 'array', $methods);
89
        }
90
91
        if ($type === 'Traversable' || $type === '\\Traversable') {
92
            $type = 'Iterator';
93
        }
94
95
        if (\is_array($type)) {
96
            $type = \array_unique(
97
                \array_map(
98
                    function ($type) {
99
                        if ($type === 'Traversable' ||
100
                            $type === '\\Traversable' ||
101
                            $type === '\\Iterator') {
102
                            return 'Iterator';
103
                        }
104
105
                        return $type;
106
                    },
107
                    $type
108
                )
109
            );
110
        }
111
112
        if (!$allowMockingUnknownTypes) {
113
            if (\is_array($type)) {
114
                foreach ($type as $_type) {
115
                    if (!\class_exists($_type, $callAutoload) &&
116
                        !\interface_exists($_type, $callAutoload)) {
117
                        throw new RuntimeException(
118
                            \sprintf(
119
                                'Cannot stub or mock class or interface "%s" which does not exist',
120
                                $_type
121
                            )
122
                        );
123
                    }
124
                }
125
            } else {
126
                if (!\class_exists($type, $callAutoload) &&
127
                    !\interface_exists($type, $callAutoload)
128
                ) {
129
                    throw new RuntimeException(
130
                        \sprintf(
131
                            'Cannot stub or mock class or interface "%s" which does not exist',
132
                            $type
133
                        )
134
                    );
135
                }
136
            }
137
        }
138
139
        if (null !== $methods) {
140
            foreach ($methods as $method) {
141
                if (!\preg_match('~[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*~', $method)) {
142
                    throw new RuntimeException(
143
                        \sprintf(
144
                            'Cannot stub or mock method with invalid name "%s"',
145
                            $method
146
                        )
147
                    );
148
                }
149
            }
150
151
            if ($methods !== \array_unique($methods)) {
152
                throw new RuntimeException(
153
                    \sprintf(
154
                        'Cannot stub or mock using a method list that contains duplicates: "%s" (duplicate: "%s")',
155
                        \implode(', ', $methods),
156
                        \implode(', ', \array_unique(\array_diff_assoc($methods, \array_unique($methods))))
157
                    )
158
                );
159
            }
160
        }
161
162
        if ($mockClassName !== '' && \class_exists($mockClassName, false)) {
163
            $reflect = new ReflectionClass($mockClassName);
164
165
            if (!$reflect->implementsInterface(MockObject::class)) {
166
                throw new RuntimeException(
167
                    \sprintf(
168
                        'Class "%s" already exists.',
169
                        $mockClassName
170
                    )
171
                );
172
            }
173
        }
174
175
        if ($callOriginalConstructor === false && $callOriginalMethods === true) {
176
            throw new RuntimeException(
177
                'Proxying to original methods requires invoking the original constructor'
178
            );
179
        }
180
181
        $mock = $this->generate(
182
            $type,
183
            $methods,
184
            $mockClassName,
185
            $callOriginalClone,
186
            $callAutoload,
187
            $cloneArguments,
188
            $callOriginalMethods
189
        );
190
191
        return $this->getObject(
192
            $mock['code'],
193
            $mock['mockClassName'],
194
            $type,
195
            $callOriginalConstructor,
196
            $callAutoload,
197
            $arguments,
198
            $callOriginalMethods,
199
            $proxyTarget,
200
            $returnValueGeneration
201
        );
202
    }
203
204
    /**
205
     * Returns a mock object for the specified abstract class with all abstract
206
     * methods of the class mocked. Concrete methods to mock can be specified with
207
     * the last parameter
208
     *
209
     * @param string $originalClassName
210
     * @param string $mockClassName
211
     * @param bool   $callOriginalConstructor
212
     * @param bool   $callOriginalClone
213
     * @param bool   $callAutoload
214
     * @param array  $mockedMethods
215
     * @param bool   $cloneArguments
216
     *
217
     * @throws \ReflectionException
218
     * @throws RuntimeException
219
     * @throws Exception
220
     *
221
     * @return MockObject
222
     */
223
    public function getMockForAbstractClass($originalClassName, array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = [], $cloneArguments = true)
224
    {
225
        if (!\is_string($originalClassName)) {
0 ignored issues
show
introduced by
The condition is_string($originalClassName) is always true.
Loading history...
226
            throw InvalidArgumentHelper::factory(1, 'string');
227
        }
228
229
        if (!\is_string($mockClassName)) {
0 ignored issues
show
introduced by
The condition is_string($mockClassName) is always true.
Loading history...
230
            throw InvalidArgumentHelper::factory(3, 'string');
231
        }
232
233
        if (\class_exists($originalClassName, $callAutoload) ||
234
            \interface_exists($originalClassName, $callAutoload)) {
235
            $reflector = new ReflectionClass($originalClassName);
236
            $methods   = $mockedMethods;
237
238
            foreach ($reflector->getMethods() as $method) {
239
                if ($method->isAbstract() && !\in_array($method->getName(), $methods, true)) {
240
                    $methods[] = $method->getName();
241
                }
242
            }
243
244
            if (empty($methods)) {
245
                $methods = null;
246
            }
247
248
            return $this->getMock(
249
                $originalClassName,
250
                $methods,
251
                $arguments,
252
                $mockClassName,
253
                $callOriginalConstructor,
254
                $callOriginalClone,
255
                $callAutoload,
256
                $cloneArguments
257
            );
258
        }
259
260
        throw new RuntimeException(
261
            \sprintf('Class "%s" does not exist.', $originalClassName)
262
        );
263
    }
264
265
    /**
266
     * Returns a mock object for the specified trait with all abstract methods
267
     * of the trait mocked. Concrete methods to mock can be specified with the
268
     * `$mockedMethods` parameter.
269
     *
270
     * @param string $traitName
271
     * @param string $mockClassName
272
     * @param bool   $callOriginalConstructor
273
     * @param bool   $callOriginalClone
274
     * @param bool   $callAutoload
275
     * @param array  $mockedMethods
276
     * @param bool   $cloneArguments
277
     *
278
     * @throws \ReflectionException
279
     * @throws RuntimeException
280
     * @throws Exception
281
     *
282
     * @return MockObject
283
     */
284
    public function getMockForTrait($traitName, array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = [], $cloneArguments = true)
285
    {
286
        if (!\is_string($traitName)) {
0 ignored issues
show
introduced by
The condition is_string($traitName) is always true.
Loading history...
287
            throw InvalidArgumentHelper::factory(1, 'string');
288
        }
289
290
        if (!\is_string($mockClassName)) {
0 ignored issues
show
introduced by
The condition is_string($mockClassName) is always true.
Loading history...
291
            throw InvalidArgumentHelper::factory(3, 'string');
292
        }
293
294
        if (!\trait_exists($traitName, $callAutoload)) {
295
            throw new RuntimeException(
296
                \sprintf(
297
                    'Trait "%s" does not exist.',
298
                    $traitName
299
                )
300
            );
301
        }
302
303
        $className = $this->generateClassName(
304
            $traitName,
305
            '',
306
            'Trait_'
307
        );
308
309
        $classTemplate = $this->getTemplate('trait_class.tpl');
310
311
        $classTemplate->setVar(
312
            [
313
                'prologue'   => 'abstract ',
314
                'class_name' => $className['className'],
315
                'trait_name' => $traitName,
316
            ]
317
        );
318
319
        $this->evalClass(
320
            $classTemplate->render(),
321
            $className['className']
322
        );
323
324
        return $this->getMockForAbstractClass($className['className'], $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $mockedMethods, $cloneArguments);
325
    }
326
327
    /**
328
     * Returns an object for the specified trait.
329
     *
330
     * @param string $traitName
331
     * @param string $traitClassName
332
     * @param bool   $callOriginalConstructor
333
     * @param bool   $callOriginalClone
334
     * @param bool   $callAutoload
335
     *
336
     * @throws \ReflectionException
337
     * @throws RuntimeException
338
     * @throws Exception
339
     *
340
     * @return object
341
     */
342
    public function getObjectForTrait($traitName, array $arguments = [], $traitClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true)
0 ignored issues
show
Unused Code introduced by
The parameter $callOriginalClone is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

342
    public function getObjectForTrait($traitName, array $arguments = [], $traitClassName = '', $callOriginalConstructor = true, /** @scrutinizer ignore-unused */ $callOriginalClone = true, $callAutoload = true)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
343
    {
344
        if (!\is_string($traitName)) {
0 ignored issues
show
introduced by
The condition is_string($traitName) is always true.
Loading history...
345
            throw InvalidArgumentHelper::factory(1, 'string');
346
        }
347
348
        if (!\is_string($traitClassName)) {
0 ignored issues
show
introduced by
The condition is_string($traitClassName) is always true.
Loading history...
349
            throw InvalidArgumentHelper::factory(3, 'string');
350
        }
351
352
        if (!\trait_exists($traitName, $callAutoload)) {
353
            throw new RuntimeException(
354
                \sprintf(
355
                    'Trait "%s" does not exist.',
356
                    $traitName
357
                )
358
            );
359
        }
360
361
        $className = $this->generateClassName(
362
            $traitName,
363
            $traitClassName,
364
            'Trait_'
365
        );
366
367
        $classTemplate = $this->getTemplate('trait_class.tpl');
368
369
        $classTemplate->setVar(
370
            [
371
                'prologue'   => '',
372
                'class_name' => $className['className'],
373
                'trait_name' => $traitName,
374
            ]
375
        );
376
377
        return $this->getObject(
378
            $classTemplate->render(),
379
            $className['className'],
380
            '',
381
            $callOriginalConstructor,
382
            $callAutoload,
383
            $arguments
384
        );
385
    }
386
387
    /**
388
     * @param array|string $type
389
     * @param array        $methods
390
     * @param string       $mockClassName
391
     * @param bool         $callOriginalClone
392
     * @param bool         $callAutoload
393
     * @param bool         $cloneArguments
394
     * @param bool         $callOriginalMethods
395
     *
396
     * @throws \ReflectionException
397
     * @throws \PHPUnit\Framework\MockObject\RuntimeException
398
     *
399
     * @return array
400
     */
401
    public function generate($type, array $methods = null, $mockClassName = '', $callOriginalClone = true, $callAutoload = true, $cloneArguments = true, $callOriginalMethods = false)
402
    {
403
        if (\is_array($type)) {
404
            \sort($type);
405
        }
406
407
        if ($mockClassName !== '') {
408
            return $this->generateMock(
409
                $type,
410
                $methods,
411
                $mockClassName,
412
                $callOriginalClone,
413
                $callAutoload,
414
                $cloneArguments,
415
                $callOriginalMethods
416
            );
417
        }
418
        $key = \md5(
419
            \is_array($type) ? \implode('_', $type) : $type .
420
            \serialize($methods) .
421
            \serialize($callOriginalClone) .
422
            \serialize($cloneArguments) .
423
            \serialize($callOriginalMethods)
424
        );
425
426
        if (!isset(self::$cache[$key])) {
427
            self::$cache[$key] = $this->generateMock(
428
                $type,
429
                $methods,
430
                $mockClassName,
431
                $callOriginalClone,
432
                $callAutoload,
433
                $cloneArguments,
434
                $callOriginalMethods
435
            );
436
        }
437
438
        return self::$cache[$key];
439
    }
440
441
    /**
442
     * @param string $wsdlFile
443
     * @param string $className
444
     *
445
     * @throws RuntimeException
446
     *
447
     * @return string
448
     */
449
    public function generateClassFromWsdl($wsdlFile, $className, array $methods = [], array $options = [])
450
    {
451
        if (!\extension_loaded('soap')) {
452
            throw new RuntimeException(
453
                'The SOAP extension is required to generate a mock object from WSDL.'
454
            );
455
        }
456
457
        $options  = \array_merge($options, ['cache_wsdl' => \WSDL_CACHE_NONE]);
458
        $client   = new SoapClient($wsdlFile, $options);
459
        $_methods = \array_unique($client->__getFunctions());
460
        unset($client);
461
462
        \sort($_methods);
463
464
        $methodTemplate = $this->getTemplate('wsdl_method.tpl');
465
        $methodsBuffer  = '';
466
467
        foreach ($_methods as $method) {
468
            \preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*\(/', $method, $matches, \PREG_OFFSET_CAPTURE);
469
            $lastFunction = \array_pop($matches[0]);
470
            $nameStart    = $lastFunction[1];
471
            $nameEnd      = $nameStart + \strlen($lastFunction[0]) - 1;
472
            $name         = \str_replace('(', '', $lastFunction[0]);
473
474
            if (empty($methods) || \in_array($name, $methods, true)) {
475
                $args = \explode(
476
                    ',',
477
                    \str_replace(')', '', \substr($method, $nameEnd + 1))
478
                );
479
480
                foreach (\range(0, \count($args) - 1) as $i) {
481
                    $args[$i] = \substr($args[$i], \strpos($args[$i], '$'));
482
                }
483
484
                $methodTemplate->setVar(
485
                    [
486
                        'method_name' => $name,
487
                        'arguments'   => \implode(', ', $args),
488
                    ]
489
                );
490
491
                $methodsBuffer .= $methodTemplate->render();
492
            }
493
        }
494
495
        $optionsBuffer = '[';
496
497
        foreach ($options as $key => $value) {
498
            $optionsBuffer .= $key . ' => ' . $value;
499
        }
500
501
        $optionsBuffer .= ']';
502
503
        $classTemplate = $this->getTemplate('wsdl_class.tpl');
504
        $namespace     = '';
505
506
        if (\strpos($className, '\\') !== false) {
507
            $parts     = \explode('\\', $className);
508
            $className = \array_pop($parts);
509
            $namespace = 'namespace ' . \implode('\\', $parts) . ';' . "\n\n";
510
        }
511
512
        $classTemplate->setVar(
513
            [
514
                'namespace'  => $namespace,
515
                'class_name' => $className,
516
                'wsdl'       => $wsdlFile,
517
                'options'    => $optionsBuffer,
518
                'methods'    => $methodsBuffer,
519
            ]
520
        );
521
522
        return $classTemplate->render();
523
    }
524
525
    /**
526
     * @param string $className
527
     *
528
     * @throws \ReflectionException
529
     *
530
     * @return string[]
531
     */
532
    public function getClassMethods($className): array
533
    {
534
        $class   = new ReflectionClass($className);
535
        $methods = [];
536
537
        foreach ($class->getMethods() as $method) {
538
            if ($method->isPublic() || $method->isAbstract()) {
539
                $methods[] = $method->getName();
540
            }
541
        }
542
543
        return $methods;
544
    }
545
546
    /**
547
     * @throws \ReflectionException
548
     *
549
     * @return MockMethod[]
550
     */
551
    public function mockClassMethods(string $className, bool $callOriginalMethods, bool $cloneArguments): array
552
    {
553
        $class   = new ReflectionClass($className);
554
        $methods = [];
555
556
        foreach ($class->getMethods() as $method) {
557
            if (($method->isPublic() || $method->isAbstract()) && $this->canMockMethod($method)) {
558
                $methods[] = MockMethod::fromReflection($method, $callOriginalMethods, $cloneArguments);
559
            }
560
        }
561
562
        return $methods;
563
    }
564
565
    /**
566
     * @throws \ReflectionException
567
     *
568
     * @return \ReflectionMethod[]
569
     */
570
    private function userDefinedInterfaceMethods(string $interfaceName): array
571
    {
572
        $interface = new ReflectionClass($interfaceName);
573
        $methods   = [];
574
575
        foreach ($interface->getMethods() as $method) {
576
            if (!$method->isUserDefined()) {
577
                continue;
578
            }
579
580
            $methods[] = $method;
581
        }
582
583
        return $methods;
584
    }
585
586
    /**
587
     * @param string       $code
588
     * @param string       $className
589
     * @param array|string $type
590
     * @param bool         $callOriginalConstructor
591
     * @param bool         $callAutoload
592
     * @param bool         $callOriginalMethods
593
     * @param object       $proxyTarget
594
     * @param bool         $returnValueGeneration
595
     *
596
     * @throws \ReflectionException
597
     * @throws RuntimeException
598
     *
599
     * @return MockObject
600
     */
601
    private function getObject($code, $className, $type = '', $callOriginalConstructor = false, $callAutoload = false, array $arguments = [], $callOriginalMethods = false, $proxyTarget = null, $returnValueGeneration = true)
0 ignored issues
show
Unused Code introduced by
The parameter $callAutoload is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

601
    private function getObject($code, $className, $type = '', $callOriginalConstructor = false, /** @scrutinizer ignore-unused */ $callAutoload = false, array $arguments = [], $callOriginalMethods = false, $proxyTarget = null, $returnValueGeneration = true)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
602
    {
603
        $this->evalClass($code, $className);
604
605
        if ($callOriginalConstructor) {
606
            if (\count($arguments) === 0) {
607
                $object = new $className;
608
            } else {
609
                $class  = new ReflectionClass($className);
610
                $object = $class->newInstanceArgs($arguments);
611
            }
612
        } else {
613
            try {
614
                $instantiator = new Instantiator;
615
                $object       = $instantiator->instantiate($className);
616
            } catch (InstantiatorException $exception) {
617
                throw new RuntimeException($exception->getMessage());
618
            }
619
        }
620
621
        if ($callOriginalMethods) {
622
            if (!\is_object($proxyTarget)) {
623
                if (\count($arguments) === 0) {
624
                    $proxyTarget = new $type;
625
                } else {
626
                    $class       = new ReflectionClass($type);
0 ignored issues
show
Bug introduced by
It seems like $type can also be of type array; however, parameter $objectOrClass of ReflectionClass::__construct() does only seem to accept object|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

626
                    $class       = new ReflectionClass(/** @scrutinizer ignore-type */ $type);
Loading history...
627
                    $proxyTarget = $class->newInstanceArgs($arguments);
628
                }
629
            }
630
631
            $object->__phpunit_setOriginalObject($proxyTarget);
632
        }
633
634
        if ($object instanceof MockObject) {
635
            $object->__phpunit_setReturnValueGeneration($returnValueGeneration);
636
        }
637
638
        return $object;
639
    }
640
641
    /**
642
     * @param string $code
643
     * @param string $className
644
     */
645
    private function evalClass($code, $className): void
646
    {
647
        if (!\class_exists($className, false)) {
648
            eval($code);
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
649
        }
650
    }
651
652
    /**
653
     * @param array|string $type
654
     * @param null|array   $explicitMethods
655
     * @param string       $mockClassName
656
     * @param bool         $callOriginalClone
657
     * @param bool         $callAutoload
658
     * @param bool         $cloneArguments
659
     * @param bool         $callOriginalMethods
660
     *
661
     * @throws \InvalidArgumentException
662
     * @throws \ReflectionException
663
     * @throws RuntimeException
664
     *
665
     * @return array
666
     */
667
    private function generateMock($type, $explicitMethods, $mockClassName, $callOriginalClone, $callAutoload, $cloneArguments, $callOriginalMethods)
668
    {
669
        $classTemplate       = $this->getTemplate('mocked_class.tpl');
670
671
        $additionalInterfaces = [];
672
        $cloneTemplate        = '';
673
        $isClass              = false;
674
        $isInterface          = false;
675
        $class                = null;
676
        $mockMethods          = new MockMethodSet;
677
678
        if (\is_array($type)) {
679
            $interfaceMethods = [];
680
681
            foreach ($type as $_type) {
682
                if (!\interface_exists($_type, $callAutoload)) {
683
                    throw new RuntimeException(
684
                        \sprintf(
685
                            'Interface "%s" does not exist.',
686
                            $_type
687
                        )
688
                    );
689
                }
690
691
                $additionalInterfaces[] = $_type;
692
                $typeClass              = new ReflectionClass($_type);
693
694
                foreach ($this->getClassMethods($_type) as $method) {
695
                    if (\in_array($method, $interfaceMethods, true)) {
696
                        throw new RuntimeException(
697
                            \sprintf(
698
                                'Duplicate method "%s" not allowed.',
699
                                $method
700
                            )
701
                        );
702
                    }
703
704
                    $methodReflection = $typeClass->getMethod($method);
705
706
                    if ($this->canMockMethod($methodReflection)) {
707
                        $mockMethods->addMethods(
708
                            MockMethod::fromReflection($methodReflection, $callOriginalMethods, $cloneArguments)
709
                        );
710
711
                        $interfaceMethods[] = $method;
712
                    }
713
                }
714
            }
715
716
            unset($interfaceMethods);
717
        }
718
719
        $mockClassName = $this->generateClassName(
720
            $type,
721
            $mockClassName,
722
            'Mock_'
723
        );
724
725
        if (\class_exists($mockClassName['fullClassName'], $callAutoload)) {
726
            $isClass = true;
727
        } elseif (\interface_exists($mockClassName['fullClassName'], $callAutoload)) {
728
            $isInterface = true;
729
        }
730
731
        if (!$isClass && !$isInterface) {
732
            $prologue = 'class ' . $mockClassName['originalClassName'] . "\n{\n}\n\n";
733
734
            if (!empty($mockClassName['namespaceName'])) {
735
                $prologue = 'namespace ' . $mockClassName['namespaceName'] .
736
                            " {\n\n" . $prologue . "}\n\n" .
737
                            "namespace {\n\n";
738
739
                $epilogue = "\n\n}";
740
            }
741
742
            $cloneTemplate = $this->getTemplate('mocked_clone.tpl');
743
        } else {
744
            $class = new ReflectionClass($mockClassName['fullClassName']);
745
746
            if ($class->isFinal()) {
747
                throw new RuntimeException(
748
                    \sprintf(
749
                        'Class "%s" is declared "final" and cannot be mocked.',
750
                        $mockClassName['fullClassName']
751
                    )
752
                );
753
            }
754
755
            // @see https://github.com/sebastianbergmann/phpunit/issues/2995
756
            if ($isInterface && $class->implementsInterface(\Throwable::class)) {
757
                $actualClassName        = \Exception::class;
758
                $additionalInterfaces[] = $class->getName();
759
                $isInterface            = false;
760
761
                try {
762
                    $class = new \ReflectionClass($actualClassName);
763
                } catch (\ReflectionException $e) {
764
                    throw new RuntimeException(
765
                        $e->getMessage(),
766
                        (int) $e->getCode(),
767
                        $e
768
                    );
769
                }
770
771
                foreach ($this->userDefinedInterfaceMethods($mockClassName['fullClassName']) as $method) {
772
                    $methodName = $method->getName();
773
774
                    if ($class->hasMethod($methodName)) {
775
                        try {
776
                            $classMethod = $class->getMethod($methodName);
777
                        } catch (\ReflectionException $e) {
778
                            throw new RuntimeException(
779
                                $e->getMessage(),
780
                                (int) $e->getCode(),
781
                                $e
782
                            );
783
                        }
784
785
                        if (!$this->canMockMethod($classMethod)) {
786
                            continue;
787
                        }
788
                    }
789
790
                    $mockMethods->addMethods(
791
                        MockMethod::fromReflection($method, $callOriginalMethods, $cloneArguments)
792
                    );
793
                }
794
795
                $mockClassName = $this->generateClassName(
796
                    $actualClassName,
797
                    $mockClassName['className'],
798
                    'Mock_'
799
                );
800
            }
801
802
            // https://github.com/sebastianbergmann/phpunit-mock-objects/issues/103
803
            if ($isInterface && $class->implementsInterface(Traversable::class) &&
804
                !$class->implementsInterface(Iterator::class) &&
805
                !$class->implementsInterface(IteratorAggregate::class)) {
806
                $additionalInterfaces[] = Iterator::class;
807
808
                $mockMethods->addMethods(
809
                    ...$this->mockClassMethods(Iterator::class, $callOriginalMethods, $cloneArguments)
810
                );
811
            }
812
813
            if ($class->hasMethod('__clone')) {
814
                $cloneMethod = $class->getMethod('__clone');
815
816
                if (!$cloneMethod->isFinal()) {
817
                    if ($callOriginalClone && !$isInterface) {
818
                        $cloneTemplate = $this->getTemplate('unmocked_clone.tpl');
819
                    } else {
820
                        $cloneTemplate = $this->getTemplate('mocked_clone.tpl');
821
                    }
822
                }
823
            } else {
824
                $cloneTemplate = $this->getTemplate('mocked_clone.tpl');
825
            }
826
        }
827
828
        if (\is_object($cloneTemplate)) {
829
            $cloneTemplate = $cloneTemplate->render();
830
        }
831
832
        if ($explicitMethods === [] &&
833
            ($isClass || $isInterface)) {
834
            $mockMethods->addMethods(
835
                ...$this->mockClassMethods($mockClassName['fullClassName'], $callOriginalMethods, $cloneArguments)
836
            );
837
        }
838
839
        if (\is_array($explicitMethods)) {
840
            foreach ($explicitMethods as $methodName) {
841
                if ($class !== null && $class->hasMethod($methodName)) {
842
                    $method = $class->getMethod($methodName);
843
844
                    if ($this->canMockMethod($method)) {
845
                        $mockMethods->addMethods(
846
                            MockMethod::fromReflection($method, $callOriginalMethods, $cloneArguments)
847
                        );
848
                    }
849
                } else {
850
                    $mockMethods->addMethods(
851
                        MockMethod::fromName(
852
                            $mockClassName['fullClassName'],
853
                            $methodName,
854
                            $cloneArguments
855
                        )
856
                    );
857
                }
858
            }
859
        }
860
861
        $mockedMethods = '';
862
        $configurable  = [];
863
864
        /** @var MockMethod $mockMethod */
865
        foreach ($mockMethods->asArray() as $mockMethod) {
866
            $mockedMethods .= $mockMethod->generateCode();
867
            $configurable[] = \strtolower($mockMethod->getName());
868
        }
869
870
        $method = '';
871
872
        if (!$mockMethods->hasMethod('method') && (!isset($class) || !$class->hasMethod('method'))) {
873
            $methodTemplate = $this->getTemplate('mocked_class_method.tpl');
874
875
            $method = $methodTemplate->render();
876
        }
877
878
        $classTemplate->setVar(
879
            [
880
                'prologue'          => $prologue ?? '',
881
                'epilogue'          => $epilogue ?? '',
882
                'class_declaration' => $this->generateMockClassDeclaration(
883
                    $mockClassName,
884
                    $isInterface,
885
                    $additionalInterfaces
886
                ),
887
                'clone'             => $cloneTemplate,
888
                'mock_class_name'   => $mockClassName['className'],
889
                'mocked_methods'    => $mockedMethods,
890
                'method'            => $method,
891
                'configurable'      => '[' . \implode(
892
                    ', ',
893
                    \array_map(
894
                        function ($m) {
895
                            return '\'' . $m . '\'';
896
                        },
897
                        $configurable
898
                    )
899
                ) . ']',
900
            ]
901
        );
902
903
        return [
904
            'code'          => $classTemplate->render(),
905
            'mockClassName' => $mockClassName['className'],
906
        ];
907
    }
908
909
    /**
910
     * @param array|string $type
911
     * @param string       $className
912
     * @param string       $prefix
913
     *
914
     * @return array
915
     */
916
    private function generateClassName($type, $className, $prefix)
917
    {
918
        if (\is_array($type)) {
919
            $type = \implode('_', $type);
920
        }
921
922
        if ($type[0] === '\\') {
923
            $type = \substr($type, 1);
924
        }
925
926
        $classNameParts = \explode('\\', $type);
927
928
        if (\count($classNameParts) > 1) {
929
            $type          = \array_pop($classNameParts);
930
            $namespaceName = \implode('\\', $classNameParts);
931
            $fullClassName = $namespaceName . '\\' . $type;
932
        } else {
933
            $namespaceName = '';
934
            $fullClassName = $type;
935
        }
936
937
        if ($className === '') {
938
            do {
939
                $className = $prefix . $type . '_' .
940
                             \substr(\md5(\mt_rand()), 0, 8);
941
            } while (\class_exists($className, false));
942
        }
943
944
        return [
945
            'className'         => $className,
946
            'originalClassName' => $type,
947
            'fullClassName'     => $fullClassName,
948
            'namespaceName'     => $namespaceName,
949
        ];
950
    }
951
952
    /**
953
     * @param bool $isInterface
954
     *
955
     * @return string
956
     */
957
    private function generateMockClassDeclaration(array $mockClassName, $isInterface, array $additionalInterfaces = [])
958
    {
959
        $buffer = 'class ';
960
961
        $additionalInterfaces[] = MockObject::class;
962
        $interfaces             = \implode(', ', $additionalInterfaces);
963
964
        if ($isInterface) {
965
            $buffer .= \sprintf(
966
                '%s implements %s',
967
                $mockClassName['className'],
968
                $interfaces
969
            );
970
971
            if (!\in_array($mockClassName['originalClassName'], $additionalInterfaces)) {
972
                $buffer .= ', ';
973
974
                if (!empty($mockClassName['namespaceName'])) {
975
                    $buffer .= $mockClassName['namespaceName'] . '\\';
976
                }
977
978
                $buffer .= $mockClassName['originalClassName'];
979
            }
980
        } else {
981
            $buffer .= \sprintf(
982
                '%s extends %s%s implements %s',
983
                $mockClassName['className'],
984
                !empty($mockClassName['namespaceName']) ? $mockClassName['namespaceName'] . '\\' : '',
985
                $mockClassName['originalClassName'],
986
                $interfaces
987
            );
988
        }
989
990
        return $buffer;
991
    }
992
993
    /**
994
     * @return bool
995
     */
996
    private function canMockMethod(ReflectionMethod $method)
997
    {
998
        return !($method->isConstructor() || $method->isFinal() || $method->isPrivate() || $this->isMethodNameBlacklisted($method->getName()));
999
    }
1000
1001
    /**
1002
     * Returns whether a method name is blacklisted
1003
     *
1004
     * @param string $name
1005
     *
1006
     * @return bool
1007
     */
1008
    private function isMethodNameBlacklisted($name)
1009
    {
1010
        return isset(self::BLACKLISTED_METHOD_NAMES[$name]);
1011
    }
1012
1013
    /**
1014
     * @param string $template
1015
     *
1016
     * @throws \InvalidArgumentException
1017
     *
1018
     * @return Text_Template
1019
     */
1020
    private function getTemplate($template)
1021
    {
1022
        $filename = __DIR__ . \DIRECTORY_SEPARATOR . 'Generator' . \DIRECTORY_SEPARATOR . $template;
1023
1024
        if (!isset(self::$templates[$filename])) {
1025
            self::$templates[$filename] = new Text_Template($filename);
1026
        }
1027
1028
        return self::$templates[$filename];
1029
    }
1030
}
1031