Completed
Push — master ( 96d573...f9f049 )
by Ehsan
07:54
created

getTemplate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/*
3
 * This file is part of the phpunit-mock-objects package.
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
11
use Doctrine\Instantiator\Instantiator;
12
use Doctrine\Instantiator\Exception\InvalidArgumentException as InstantiatorInvalidArgumentException;
13
use Doctrine\Instantiator\Exception\UnexpectedValueException as InstantiatorUnexpectedValueException;
14
use PHPUnit\Util\InvalidArgumentHelper;
15
16
/**
17
 * Mock Object Code Generator
18
 */
19
class PHPUnit_Framework_MockObject_Generator
20
{
21
    /**
22
     * @var array
23
     */
24
    private static $cache = [];
25
26
    /**
27
     * @var Text_Template[]
28
     */
29
    private static $templates = [];
30
31
    /**
32
     * @var array
33
     */
34
    private $blacklistedMethodNames = [
35
        '__CLASS__'       => true,
36
        '__DIR__'         => true,
37
        '__FILE__'        => true,
38
        '__FUNCTION__'    => true,
39
        '__LINE__'        => true,
40
        '__METHOD__'      => true,
41
        '__NAMESPACE__'   => true,
42
        '__TRAIT__'       => true,
43
        '__clone'         => true,
44
        '__halt_compiler' => true,
45
    ];
46
47
    /**
48
     * Returns a mock object for the specified class.
49
     *
50
     * @param array|string $type
51
     * @param array        $methods
52
     * @param array        $arguments
53
     * @param string       $mockClassName
54
     * @param bool         $callOriginalConstructor
55
     * @param bool         $callOriginalClone
56
     * @param bool         $callAutoload
57
     * @param bool         $cloneArguments
58
     * @param bool         $callOriginalMethods
59
     * @param object       $proxyTarget
60
     * @param bool         $allowMockingUnknownTypes
61
     *
62
     * @return PHPUnit_Framework_MockObject_MockObject
63
     *
64
     * @throws InvalidArgumentException
65
     * @throws PHPUnit\Framework\Exception
66
     * @throws PHPUnit_Framework_MockObject_RuntimeException
67
     */
68
    public function getMock($type, $methods = [], array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $cloneArguments = true, $callOriginalMethods = false, $proxyTarget = null, $allowMockingUnknownTypes = true)
69
    {
70
        if (!is_array($type) && !is_string($type)) {
71
            throw InvalidArgumentHelper::factory(1, 'array or string');
72
        }
73
74
        if (!is_string($mockClassName)) {
75
            throw InvalidArgumentHelper::factory(4, 'string');
76
        }
77
78
        if (!is_array($methods) && !is_null($methods)) {
79
            throw InvalidArgumentHelper::factory(2, 'array', $methods);
80
        }
81
82
        if ($type === 'Traversable' || $type === '\\Traversable') {
83
            $type = 'Iterator';
84
        }
85
86
        if (is_array($type)) {
87
            $type = array_unique(
88
                array_map(
89
                    function ($type) {
90
                        if ($type === 'Traversable' ||
91
                            $type === '\\Traversable' ||
92
                            $type === '\\Iterator') {
93
                            return 'Iterator';
94
                        }
95
96
                        return $type;
97
                    },
98
                    $type
99
                )
100
            );
101
        }
102
103
        if (!$allowMockingUnknownTypes) {
104
            if (is_array($type)) {
105
                foreach ($type as $_type) {
106
                    if (!class_exists($_type, $callAutoload) &&
107
                        !interface_exists($_type, $callAutoload)) {
108
                        throw new PHPUnit_Framework_MockObject_RuntimeException(
109
                            sprintf(
110
                                'Cannot stub or mock class or interface "%s" which does not exist',
111
                                $_type
112
                            )
113
                        );
114
                    }
115
                }
116
            } else {
117
                if (!class_exists($type, $callAutoload) &&
118
                    !interface_exists($type, $callAutoload)
119
                ) {
120
                    throw new PHPUnit_Framework_MockObject_RuntimeException(
121
                        sprintf(
122
                            'Cannot stub or mock class or interface "%s" which does not exist',
123
                            $type
124
                        )
125
                    );
126
                }
127
            }
128
        }
129
130
        if (null !== $methods) {
131
            foreach ($methods as $method) {
132
                if (!preg_match('~[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*~', $method)) {
133
                    throw new PHPUnit_Framework_MockObject_RuntimeException(
134
                        sprintf(
135
                            'Cannot stub or mock method with invalid name "%s"',
136
                            $method
137
                        )
138
                    );
139
                }
140
            }
141
142
            if ($methods != array_unique($methods)) {
143
                throw new PHPUnit_Framework_MockObject_RuntimeException(
144
                    sprintf(
145
                        'Cannot stub or mock using a method list that contains duplicates: "%s" (duplicate: "%s")',
146
                        implode(', ', $methods),
147
                        implode(', ', array_unique(array_diff_assoc($methods, array_unique($methods))))
148
                    )
149
                );
150
            }
151
        }
152
153
        if ($mockClassName != '' && class_exists($mockClassName, false)) {
154
            $reflect = new ReflectionClass($mockClassName);
155
156
            if (!$reflect->implementsInterface('PHPUnit_Framework_MockObject_MockObject')) {
157
                throw new PHPUnit_Framework_MockObject_RuntimeException(
158
                    sprintf(
159
                        'Class "%s" already exists.',
160
                        $mockClassName
161
                    )
162
                );
163
            }
164
        }
165
166
        if ($callOriginalConstructor === false && $callOriginalMethods === true) {
167
            throw new PHPUnit_Framework_MockObject_RuntimeException(
168
                'Proxying to original methods requires invoking the original constructor'
169
            );
170
        }
171
172
        $mock = $this->generate(
173
            $type,
174
            $methods,
175
            $mockClassName,
176
            $callOriginalClone,
177
            $callAutoload,
178
            $cloneArguments,
179
            $callOriginalMethods
180
        );
181
182
        return $this->getObject(
183
            $mock['code'],
184
            $mock['mockClassName'],
185
            $type,
186
            $callOriginalConstructor,
187
            $callAutoload,
188
            $arguments,
189
            $callOriginalMethods,
190
            $proxyTarget
191
        );
192
    }
193
194
    /**
195
     * @param string       $code
196
     * @param string       $className
197
     * @param array|string $type
198
     * @param bool         $callOriginalConstructor
199
     * @param bool         $callAutoload
200
     * @param array        $arguments
201
     * @param bool         $callOriginalMethods
202
     * @param object       $proxyTarget
203
     *
204
     * @return PHPUnit_Framework_MockObject_MockObject
205
     *
206
     * @throws PHPUnit_Framework_MockObject_RuntimeException
207
     */
208
    private function getObject($code, $className, $type = '', $callOriginalConstructor = false, $callAutoload = false, array $arguments = [], $callOriginalMethods = false, $proxyTarget = null)
209
    {
210
        $this->evalClass($code, $className);
211
212
        if ($callOriginalConstructor &&
213
            is_string($type) &&
214
            !interface_exists($type, $callAutoload)) {
215
            if (count($arguments) == 0) {
216
                $object = new $className;
217
            } else {
218
                $class  = new ReflectionClass($className);
219
                $object = $class->newInstanceArgs($arguments);
220
            }
221
        } else {
222
            try {
223
                $instantiator = new Instantiator;
224
                $object       = $instantiator->instantiate($className);
225
            } catch (InstantiatorUnexpectedValueException $exception) {
226
                if ($exception->getPrevious()) {
227
                    $exception = $exception->getPrevious();
228
                }
229
230
                throw new PHPUnit_Framework_MockObject_RuntimeException(
231
                    $exception->getMessage()
232
                );
233
            } catch (InstantiatorInvalidArgumentException $exception) {
234
                throw new PHPUnit_Framework_MockObject_RuntimeException(
235
                    $exception->getMessage()
236
                );
237
            }
238
        }
239
240
        if ($callOriginalMethods) {
241
            if (!is_object($proxyTarget)) {
242
                if (count($arguments) == 0) {
243
                    $proxyTarget = new $type;
244
                } else {
245
                    $class       = new ReflectionClass($type);
246
                    $proxyTarget = $class->newInstanceArgs($arguments);
247
                }
248
            }
249
250
            $object->__phpunit_setOriginalObject($proxyTarget);
251
        }
252
253
        return $object;
254
    }
255
256
    /**
257
     * @param string $code
258
     * @param string $className
259
     */
260
    private function evalClass($code, $className)
261
    {
262
        if (!class_exists($className, false)) {
263
            eval($code);
264
        }
265
    }
266
267
    /**
268
     * Returns a mock object for the specified abstract class with all abstract
269
     * methods of the class mocked. Concrete methods to mock can be specified with
270
     * the last parameter
271
     *
272
     * @param string $originalClassName
273
     * @param array  $arguments
274
     * @param string $mockClassName
275
     * @param bool   $callOriginalConstructor
276
     * @param bool   $callOriginalClone
277
     * @param bool   $callAutoload
278
     * @param array  $mockedMethods
279
     * @param bool   $cloneArguments
280
     *
281
     * @return PHPUnit_Framework_MockObject_MockObject
282
     *
283
     * @throws PHPUnit_Framework_MockObject_RuntimeException
284
     * @throws PHPUnit\Framework\Exception
285
     */
286
    public function getMockForAbstractClass($originalClassName, array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = [], $cloneArguments = true)
287
    {
288
        if (!is_string($originalClassName)) {
289
            throw InvalidArgumentHelper::factory(1, 'string');
290
        }
291
292
        if (!is_string($mockClassName)) {
293
            throw InvalidArgumentHelper::factory(3, 'string');
294
        }
295
296
        if (class_exists($originalClassName, $callAutoload) ||
297
            interface_exists($originalClassName, $callAutoload)) {
298
            $reflector = new ReflectionClass($originalClassName);
299
            $methods   = $mockedMethods;
300
301
            foreach ($reflector->getMethods() as $method) {
302
                if ($method->isAbstract() && !in_array($method->getName(), $methods)) {
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
303
                    $methods[] = $method->getName();
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
304
                }
305
            }
306
307
            if (empty($methods)) {
308
                $methods = null;
309
            }
310
311
            return $this->getMock(
312
                $originalClassName,
313
                $methods,
0 ignored issues
show
Bug introduced by
It seems like $methods defined by null on line 308 can also be of type null; however, PHPUnit_Framework_MockObject_Generator::getMock() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
314
                $arguments,
315
                $mockClassName,
316
                $callOriginalConstructor,
317
                $callOriginalClone,
318
                $callAutoload,
319
                $cloneArguments
320
            );
321
        } else {
322
            throw new PHPUnit_Framework_MockObject_RuntimeException(
323
                sprintf('Class "%s" does not exist.', $originalClassName)
324
            );
325
        }
326
    }
327
328
    /**
329
     * Returns a mock object for the specified trait with all abstract methods
330
     * of the trait mocked. Concrete methods to mock can be specified with the
331
     * `$mockedMethods` parameter.
332
     *
333
     * @param string $traitName
334
     * @param array  $arguments
335
     * @param string $mockClassName
336
     * @param bool   $callOriginalConstructor
337
     * @param bool   $callOriginalClone
338
     * @param bool   $callAutoload
339
     * @param array  $mockedMethods
340
     * @param bool   $cloneArguments
341
     *
342
     * @return PHPUnit_Framework_MockObject_MockObject
343
     *
344
     * @throws PHPUnit_Framework_MockObject_RuntimeException
345
     * @throws PHPUnit\Framework\Exception
346
     */
347
    public function getMockForTrait($traitName, array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = [], $cloneArguments = true)
348
    {
349
        if (!is_string($traitName)) {
350
            throw InvalidArgumentHelper::factory(1, 'string');
351
        }
352
353
        if (!is_string($mockClassName)) {
354
            throw InvalidArgumentHelper::factory(3, 'string');
355
        }
356
357
        if (!trait_exists($traitName, $callAutoload)) {
358
            throw new PHPUnit_Framework_MockObject_RuntimeException(
359
                sprintf(
360
                    'Trait "%s" does not exist.',
361
                    $traitName
362
                )
363
            );
364
        }
365
366
        $className = $this->generateClassName(
367
            $traitName,
368
            '',
369
            'Trait_'
370
        );
371
372
        $classTemplate = $this->getTemplate('trait_class.tpl');
373
374
        $classTemplate->setVar(
375
            [
376
                'prologue'   => 'abstract ',
377
                'class_name' => $className['className'],
378
                'trait_name' => $traitName
379
            ]
380
        );
381
382
        $this->evalClass(
383
            $classTemplate->render(),
384
            $className['className']
385
        );
386
387
        return $this->getMockForAbstractClass($className['className'], $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $mockedMethods, $cloneArguments);
388
    }
389
390
    /**
391
     * Returns an object for the specified trait.
392
     *
393
     * @param string $traitName
394
     * @param array  $arguments
395
     * @param string $traitClassName
396
     * @param bool   $callOriginalConstructor
397
     * @param bool   $callOriginalClone
398
     * @param bool   $callAutoload
399
     *
400
     * @return object
401
     *
402
     * @throws PHPUnit_Framework_MockObject_RuntimeException
403
     * @throws PHPUnit\Framework\Exception
404
     */
405
    public function getObjectForTrait($traitName, array $arguments = [], $traitClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true)
0 ignored issues
show
Unused Code introduced by
The parameter $arguments is not used and could be removed.

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

Loading history...
Unused Code introduced by
The parameter $callOriginalConstructor is not used and could be removed.

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

Loading history...
Unused Code introduced by
The parameter $callOriginalClone is not used and could be removed.

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

Loading history...
406
    {
407
        if (!is_string($traitName)) {
408
            throw InvalidArgumentHelper::factory(1, 'string');
409
        }
410
411
        if (!is_string($traitClassName)) {
412
            throw InvalidArgumentHelper::factory(3, 'string');
413
        }
414
415
        if (!trait_exists($traitName, $callAutoload)) {
416
            throw new PHPUnit_Framework_MockObject_RuntimeException(
417
                sprintf(
418
                    'Trait "%s" does not exist.',
419
                    $traitName
420
                )
421
            );
422
        }
423
424
        $className = $this->generateClassName(
425
            $traitName,
426
            $traitClassName,
427
            'Trait_'
428
        );
429
430
        $classTemplate = $this->getTemplate('trait_class.tpl');
431
432
        $classTemplate->setVar(
433
            [
434
                'prologue'   => '',
435
                'class_name' => $className['className'],
436
                'trait_name' => $traitName
437
            ]
438
        );
439
440
        return $this->getObject(
441
            $classTemplate->render(),
442
            $className['className']
443
        );
444
    }
445
446
    /**
447
     * @param array|string $type
448
     * @param array        $methods
449
     * @param string       $mockClassName
450
     * @param bool         $callOriginalClone
451
     * @param bool         $callAutoload
452
     * @param bool         $cloneArguments
453
     * @param bool         $callOriginalMethods
454
     *
455
     * @return array
456
     */
457
    public function generate($type, array $methods = null, $mockClassName = '', $callOriginalClone = true, $callAutoload = true, $cloneArguments = true, $callOriginalMethods = false)
458
    {
459
        if (is_array($type)) {
460
            sort($type);
461
        }
462
463
        if ($mockClassName == '') {
464
            $key = md5(
465
                is_array($type) ? implode('_', $type) : $type .
466
                serialize($methods) .
467
                serialize($callOriginalClone) .
468
                serialize($cloneArguments) .
469
                serialize($callOriginalMethods)
470
            );
471
472
            if (isset(self::$cache[$key])) {
473
                return self::$cache[$key];
474
            }
475
        }
476
477
        $mock = $this->generateMock(
478
            $type,
479
            $methods,
480
            $mockClassName,
481
            $callOriginalClone,
482
            $callAutoload,
483
            $cloneArguments,
484
            $callOriginalMethods
485
        );
486
487
        if (isset($key)) {
488
            self::$cache[$key] = $mock;
489
        }
490
491
        return $mock;
492
    }
493
494
    /**
495
     * @param string $wsdlFile
496
     * @param string $className
497
     * @param array  $methods
498
     * @param array  $options
499
     *
500
     * @return string
501
     *
502
     * @throws PHPUnit_Framework_MockObject_RuntimeException
503
     */
504
    public function generateClassFromWsdl($wsdlFile, $className, array $methods = [], array $options = [])
505
    {
506
        if (!extension_loaded('soap')) {
507
            throw new PHPUnit_Framework_MockObject_RuntimeException(
508
                'The SOAP extension is required to generate a mock object from WSDL.'
509
            );
510
        }
511
512
        $options  = array_merge($options, ['cache_wsdl' => WSDL_CACHE_NONE]);
513
        $client   = new SoapClient($wsdlFile, $options);
514
        $_methods = array_unique($client->__getFunctions());
515
        unset($client);
516
517
        sort($_methods);
518
519
        $methodTemplate = $this->getTemplate('wsdl_method.tpl');
520
        $methodsBuffer  = '';
521
522
        foreach ($_methods as $method) {
523
            $nameStart = strpos($method, ' ') + 1;
524
            $nameEnd   = strpos($method, '(');
525
            $name      = substr($method, $nameStart, $nameEnd - $nameStart);
526
527
            if (empty($methods) || in_array($name, $methods)) {
528
                $args    = explode(
529
                    ',',
530
                    substr(
531
                        $method,
532
                        $nameEnd + 1,
533
                        strpos($method, ')') - $nameEnd - 1
534
                    )
535
                );
536
                $numArgs = count($args);
537
538
                for ($i = 0; $i < $numArgs; $i++) {
539
                    $args[$i] = substr($args[$i], strpos($args[$i], '$'));
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 = 'array(';
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
     * @param array|string $type
585
     * @param array|null   $methods
586
     * @param string       $mockClassName
587
     * @param bool         $callOriginalClone
588
     * @param bool         $callAutoload
589
     * @param bool         $cloneArguments
590
     * @param bool         $callOriginalMethods
591
     *
592
     * @return array
593
     *
594
     * @throws PHPUnit_Framework_MockObject_RuntimeException
595
     */
596
    private function generateMock($type, $methods, $mockClassName, $callOriginalClone, $callAutoload, $cloneArguments, $callOriginalMethods)
597
    {
598
        $methodReflections   = [];
599
        $classTemplate       = $this->getTemplate('mocked_class.tpl');
600
601
        $additionalInterfaces = [];
602
        $cloneTemplate        = '';
603
        $isClass              = false;
604
        $isInterface          = false;
605
        $isMultipleInterfaces = false;
606
607
        if (is_array($type)) {
608
            foreach ($type as $_type) {
609
                if (!interface_exists($_type, $callAutoload)) {
610
                    throw new PHPUnit_Framework_MockObject_RuntimeException(
611
                        sprintf(
612
                            'Interface "%s" does not exist.',
613
                            $_type
614
                        )
615
                    );
616
                }
617
618
                $isMultipleInterfaces = true;
619
620
                $additionalInterfaces[] = $_type;
621
                $typeClass              = new ReflectionClass($this->generateClassName(
622
                    $_type,
623
                    $mockClassName,
624
                    'Mock_'
625
                    )['fullClassName']
626
                );
627
628
                foreach ($this->getClassMethods($_type) as $method) {
629
                    if (in_array($method, $methods)) {
630
                        throw new PHPUnit_Framework_MockObject_RuntimeException(
631
                            sprintf(
632
                                'Duplicate method "%s" not allowed.',
633
                                $method
634
                            )
635
                        );
636
                    }
637
638
                    $methodReflections[$method] = $typeClass->getMethod($method);
639
                    $methods[]                  = $method;
640
                }
641
            }
642
        }
643
644
        $mockClassName = $this->generateClassName(
645
            $type,
646
            $mockClassName,
647
            'Mock_'
648
        );
649
650
        if (class_exists($mockClassName['fullClassName'], $callAutoload)) {
651
            $isClass = true;
652
        } elseif (interface_exists($mockClassName['fullClassName'], $callAutoload)) {
653
            $isInterface = true;
654
        }
655
656
        if (!$isClass && !$isInterface) {
657
            $prologue = 'class ' . $mockClassName['originalClassName'] . "\n{\n}\n\n";
658
659
            if (!empty($mockClassName['namespaceName'])) {
660
                $prologue = 'namespace ' . $mockClassName['namespaceName'] .
661
                            " {\n\n" . $prologue . "}\n\n" .
662
                            "namespace {\n\n";
663
664
                $epilogue = "\n\n}";
665
            }
666
667
            $cloneTemplate = $this->getTemplate('mocked_clone.tpl');
668
        } else {
669
            $class = new ReflectionClass($mockClassName['fullClassName']);
670
671
            if ($class->isFinal()) {
672
                throw new PHPUnit_Framework_MockObject_RuntimeException(
673
                    sprintf(
674
                        'Class "%s" is declared "final" and cannot be mocked.',
675
                        $mockClassName['fullClassName']
676
                    )
677
                );
678
            }
679
680
            if ($class->hasMethod('__clone')) {
681
                $cloneMethod = $class->getMethod('__clone');
682
683
                if (!$cloneMethod->isFinal()) {
684
                    if ($callOriginalClone && !$isInterface) {
685
                        $cloneTemplate = $this->getTemplate('unmocked_clone.tpl');
686
                    } else {
687
                        $cloneTemplate = $this->getTemplate('mocked_clone.tpl');
688
                    }
689
                }
690
            } else {
691
                $cloneTemplate = $this->getTemplate('mocked_clone.tpl');
692
            }
693
        }
694
695
        if (is_object($cloneTemplate)) {
696
            $cloneTemplate = $cloneTemplate->render();
697
        }
698
699
        if (is_array($methods) && empty($methods) &&
700
            ($isClass || $isInterface)) {
701
            $methods = $this->getClassMethods($mockClassName['fullClassName']);
702
        }
703
704
        if (!is_array($methods)) {
705
            $methods = [];
706
        }
707
708
        $mockedMethods = '';
709
        $configurable  = [];
710
711
        foreach ($methods as $methodName) {
712
            if ($methodName != '__construct' && $methodName != '__clone') {
713
                $configurable[] = strtolower($methodName);
714
            }
715
        }
716
717
        if (isset($class)) {
718
            // https://github.com/sebastianbergmann/phpunit-mock-objects/issues/103
719
            if ($isInterface && $class->implementsInterface(Traversable::class) &&
720
                !$class->implementsInterface(Iterator::class) &&
721
                !$class->implementsInterface(IteratorAggregate::class)) {
722
                $additionalInterfaces[] = Iterator::class;
723
                $methods                = array_merge($methods, $this->getClassMethods(Iterator::class));
724
            }
725
726
            foreach ($methods as $methodName) {
727
                try {
728
                    $method = $class->getMethod($methodName);
729
730
                    if ($this->canMockMethod($method)) {
731
                        $mockedMethods .= $this->generateMockedMethodDefinitionFromExisting(
732
                            $method,
733
                            $cloneArguments,
734
                            $callOriginalMethods
735
                        );
736
                    }
737
                } catch (ReflectionException $e) {
738
                    $mockedMethods .= $this->generateMockedMethodDefinition(
739
                        $mockClassName['fullClassName'],
740
                        $methodName,
741
                        $cloneArguments
742
                    );
743
                }
744
            }
745
        } elseif ($isMultipleInterfaces) {
746
            foreach ($methods as $methodName) {
747
                if ($this->canMockMethod($methodReflections[$methodName])) {
748
                    $mockedMethods .= $this->generateMockedMethodDefinitionFromExisting(
749
                        $methodReflections[$methodName],
750
                        $cloneArguments,
751
                        $callOriginalMethods
752
                    );
753
                }
754
            }
755
        } else {
756
            foreach ($methods as $methodName) {
757
                $mockedMethods .= $this->generateMockedMethodDefinition(
758
                    $mockClassName['fullClassName'],
759
                    $methodName,
760
                    $cloneArguments
761
                );
762
            }
763
        }
764
765
        $method = '';
766
767
        if (!in_array('method', $methods) && (!isset($class) || !$class->hasMethod('method'))) {
0 ignored issues
show
Bug introduced by
The variable $class does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
768
            $methodTemplate = $this->getTemplate('mocked_class_method.tpl');
769
770
            $method = $methodTemplate->render();
771
        }
772
773
        $classTemplate->setVar(
774
            [
775
                'prologue'          => isset($prologue) ? $prologue : '',
776
                'epilogue'          => isset($epilogue) ? $epilogue : '',
777
                'class_declaration' => $this->generateMockClassDeclaration(
778
                    $mockClassName,
779
                    $isInterface,
780
                    $additionalInterfaces
781
                ),
782
                'clone'             => $cloneTemplate,
783
                'mock_class_name'   => $mockClassName['className'],
784
                'mocked_methods'    => $mockedMethods,
785
                'method'            => $method,
786
                'configurable'      => '[' . implode(', ', array_map(function ($m) {
787
                    return '\'' . $m . '\'';
788
                }, $configurable)) . ']'
789
            ]
790
        );
791
792
        return [
793
          'code'          => $classTemplate->render(),
794
          'mockClassName' => $mockClassName['className']
795
        ];
796
    }
797
798
    /**
799
     * @param array|string $type
800
     * @param string       $className
801
     * @param string       $prefix
802
     *
803
     * @return array
804
     */
805
    private function generateClassName($type, $className, $prefix)
806
    {
807
        if (is_array($type)) {
808
            $type = implode('_', $type);
809
        }
810
811
        if ($type[0] == '\\') {
812
            $type = substr($type, 1);
813
        }
814
815
        $classNameParts = explode('\\', $type);
816
817
        if (count($classNameParts) > 1) {
818
            $type          = array_pop($classNameParts);
819
            $namespaceName = implode('\\', $classNameParts);
820
            $fullClassName = $namespaceName . '\\' . $type;
821
        } else {
822
            $namespaceName = '';
823
            $fullClassName = $type;
824
        }
825
826
        if ($className == '') {
827
            do {
828
                $className = $prefix . $type . '_' .
829
                             substr(md5(microtime()), 0, 8);
830
            } while (class_exists($className, false));
831
        }
832
833
        return [
834
          'className'         => $className,
835
          'originalClassName' => $type,
836
          'fullClassName'     => $fullClassName,
837
          'namespaceName'     => $namespaceName
838
        ];
839
    }
840
841
    /**
842
     * @param array $mockClassName
843
     * @param bool  $isInterface
844
     * @param array $additionalInterfaces
845
     *
846
     * @return string
847
     */
848
    private function generateMockClassDeclaration(array $mockClassName, $isInterface, array $additionalInterfaces = [])
849
    {
850
        $buffer = 'class ';
851
852
        $additionalInterfaces[] = 'PHPUnit_Framework_MockObject_MockObject';
853
        $interfaces             = implode(', ', $additionalInterfaces);
854
855
        if ($isInterface) {
856
            $buffer .= sprintf(
857
                '%s implements %s',
858
                $mockClassName['className'],
859
                $interfaces
860
            );
861
862
            if (!in_array($mockClassName['originalClassName'], $additionalInterfaces)) {
863
                $buffer .= ', ';
864
865
                if (!empty($mockClassName['namespaceName'])) {
866
                    $buffer .= $mockClassName['namespaceName'] . '\\';
867
                }
868
869
                $buffer .= $mockClassName['originalClassName'];
870
            }
871
        } else {
872
            $buffer .= sprintf(
873
                '%s extends %s%s implements %s',
874
                $mockClassName['className'],
875
                !empty($mockClassName['namespaceName']) ? $mockClassName['namespaceName'] . '\\' : '',
876
                $mockClassName['originalClassName'],
877
                $interfaces
878
            );
879
        }
880
881
        return $buffer;
882
    }
883
884
    /**
885
     * @param ReflectionMethod $method
886
     * @param bool             $cloneArguments
887
     * @param bool             $callOriginalMethods
888
     *
889
     * @return string
890
     */
891
    private function generateMockedMethodDefinitionFromExisting(ReflectionMethod $method, $cloneArguments, $callOriginalMethods)
892
    {
893
        if ($method->isPrivate()) {
894
            $modifier = 'private';
895
        } elseif ($method->isProtected()) {
896
            $modifier = 'protected';
897
        } else {
898
            $modifier = 'public';
899
        }
900
901
        if ($method->isStatic()) {
902
            $modifier .= ' static';
903
        }
904
905
        if ($method->returnsReference()) {
906
            $reference = '&';
907
        } else {
908
            $reference = '';
909
        }
910
911
        if ($method->hasReturnType()) {
912
            $returnType = (string) $method->getReturnType();
913
        } else {
914
            $returnType = '';
915
        }
916
917
        if (preg_match('#\*[ \t]*+@deprecated[ \t]*+(.*?)\r?+\n[ \t]*+\*(?:[ \t]*+@|/$)#s', $method->getDocComment(), $deprecation)) {
918
            $deprecation = trim(preg_replace('#[ \t]*\r?\n[ \t]*+\*[ \t]*+#', ' ', $deprecation[1]));
919
        } else {
920
            $deprecation = false;
921
        }
922
923
        return $this->generateMockedMethodDefinition(
924
            $method->getDeclaringClass()->getName(),
0 ignored issues
show
introduced by
Consider using $method->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
925
            $method->getName(),
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
926
            $cloneArguments,
927
            $modifier,
928
            $this->getMethodParameters($method),
929
            $this->getMethodParameters($method, true),
930
            $returnType,
931
            $reference,
932
            $callOriginalMethods,
933
            $method->isStatic(),
934
            $deprecation,
935
            $method->hasReturnType() && $method->getReturnType()->allowsNull()
936
        );
937
    }
938
939
    /**
940
     * @param string      $className
941
     * @param string      $methodName
942
     * @param bool        $cloneArguments
943
     * @param string      $modifier
944
     * @param string      $argumentsForDeclaration
945
     * @param string      $argumentsForCall
946
     * @param string      $returnType
947
     * @param string      $reference
948
     * @param bool        $callOriginalMethods
949
     * @param bool        $static
950
     * @param bool|string $deprecation
951
     * @param bool        $allowsReturnNull
952
     *
953
     * @return string
954
     */
955
    private function generateMockedMethodDefinition($className, $methodName, $cloneArguments = true, $modifier = 'public', $argumentsForDeclaration = '', $argumentsForCall = '', $returnType = '', $reference = '', $callOriginalMethods = false, $static = false, $deprecation = false, $allowsReturnNull = false)
956
    {
957
        if ($static) {
958
            $templateFile = 'mocked_static_method.tpl';
959
        } else {
960
            if ($returnType === 'void') {
961
                $templateFile = sprintf(
962
                    '%s_method_void.tpl',
963
                    $callOriginalMethods ? 'proxied' : 'mocked'
964
                );
965
            } else {
966
                $templateFile = sprintf(
967
                    '%s_method.tpl',
968
                    $callOriginalMethods ? 'proxied' : 'mocked'
969
                );
970
            }
971
        }
972
973
        // Mocked interfaces returning 'self' must explicitly declare the
974
        // interface name as the return type. See
975
        // https://bugs.php.net/bug.php?id=70722
976
        if ($returnType === 'self') {
977
            $returnType = $className;
978
        }
979
980
        if (false !== $deprecation) {
981
            $deprecation         = "The $className::$methodName method is deprecated ($deprecation).";
982
            $deprecationTemplate = $this->getTemplate('deprecation.tpl');
983
984
            $deprecationTemplate->setVar(
985
                [
986
                    'deprecation' => var_export($deprecation, true),
987
                ]
988
            );
989
990
            $deprecation = $deprecationTemplate->render();
991
        }
992
993
        $template = $this->getTemplate($templateFile);
994
995
        $template->setVar(
996
            [
997
                'arguments_decl'  => $argumentsForDeclaration,
998
                'arguments_call'  => $argumentsForCall,
999
                'return_delim'    => $returnType ? ': ' : '',
1000
                'return_type'     => $allowsReturnNull ? '?' . $returnType : $returnType,
1001
                'arguments_count' => !empty($argumentsForCall) ? count(explode(',', $argumentsForCall)) : 0,
1002
                'class_name'      => $className,
1003
                'method_name'     => $methodName,
1004
                'modifier'        => $modifier,
1005
                'reference'       => $reference,
1006
                'clone_arguments' => $cloneArguments ? 'true' : 'false',
1007
                'deprecation'     => $deprecation
1008
            ]
1009
        );
1010
1011
        return $template->render();
1012
    }
1013
1014
    /**
1015
     * @param ReflectionMethod $method
1016
     *
1017
     * @return bool
1018
     */
1019
    private function canMockMethod(ReflectionMethod $method)
1020
    {
1021
        if ($method->isConstructor() ||
1022
            $method->isFinal() ||
1023
            $method->isPrivate() ||
1024
            $this->isMethodNameBlacklisted($method->getName())) {
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
1025
            return false;
1026
        }
1027
1028
        return true;
1029
    }
1030
1031
    /**
1032
     * Returns whether a method name is blacklisted
1033
     *
1034
     * @param string $name
1035
     *
1036
     * @return bool
1037
     */
1038
    private function isMethodNameBlacklisted($name)
1039
    {
1040
        return isset($this->blacklistedMethodNames[$name]);
1041
    }
1042
1043
    /**
1044
     * Returns the parameters of a function or method.
1045
     *
1046
     * @param ReflectionMethod $method
1047
     * @param bool             $forCall
1048
     *
1049
     * @return string
1050
     *
1051
     * @throws PHPUnit_Framework_MockObject_RuntimeException
1052
     */
1053
    private function getMethodParameters(ReflectionMethod $method, $forCall = false)
1054
    {
1055
        $parameters = [];
1056
1057
        foreach ($method->getParameters() as $i => $parameter) {
1058
            $name = '$' . $parameter->getName();
0 ignored issues
show
Bug introduced by
Consider using $parameter->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
1059
1060
            /* Note: PHP extensions may use empty names for reference arguments
1061
             * or "..." for methods taking a variable number of arguments.
1062
             */
1063
            if ($name === '$' || $name === '$...') {
1064
                $name = '$arg' . $i;
1065
            }
1066
1067
            if ($parameter->isVariadic()) {
1068
                if ($forCall) {
1069
                    continue;
1070
                } else {
1071
                    $name = '...' . $name;
1072
                }
1073
            }
1074
1075
            $nullable        = '';
1076
            $default         = '';
1077
            $reference       = '';
1078
            $typeDeclaration = '';
1079
1080
            if (!$forCall) {
1081
                if ($parameter->hasType() && (string) $parameter->getType() !== 'self') {
1082
                    if (version_compare(PHP_VERSION, '7.1', '>=') && $parameter->allowsNull() && !$parameter->isVariadic()) {
1083
                        $nullable = '?';
1084
                    }
1085
1086
                    $typeDeclaration = (string) $parameter->getType() . ' ';
1087
                } elseif ($parameter->isArray()) {
1088
                    $typeDeclaration = 'array ';
1089
                } elseif ($parameter->isCallable()) {
1090
                    $typeDeclaration = 'callable ';
1091
                } else {
1092
                    try {
1093
                        $class = $parameter->getClass();
1094
                    } catch (ReflectionException $e) {
1095
                        throw new PHPUnit_Framework_MockObject_RuntimeException(
1096
                            sprintf(
1097
                                'Cannot mock %s::%s() because a class or ' .
1098
                                'interface used in the signature is not loaded',
1099
                                $method->getDeclaringClass()->getName(),
0 ignored issues
show
introduced by
Consider using $method->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
1100
                                $method->getName()
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
1101
                            ),
1102
                            0,
1103
                            $e
1104
                        );
1105
                    }
1106
1107
                    if ($class !== null) {
1108
                        $typeDeclaration = $class->getName() . ' ';
0 ignored issues
show
Bug introduced by
Consider using $class->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
1109
                    }
1110
                }
1111
1112
                if (!$parameter->isVariadic()) {
1113
                    if ($parameter->isDefaultValueAvailable()) {
1114
                        $value   = $parameter->getDefaultValue();
1115
                        $default = ' = ' . var_export($value, true);
1116
                    } elseif ($parameter->isOptional()) {
1117
                        $default = ' = null';
1118
                    }
1119
                }
1120
            }
1121
1122
            if ($parameter->isPassedByReference()) {
1123
                $reference = '&';
1124
            }
1125
1126
            $parameters[] = $nullable . $typeDeclaration . $reference . $name . $default;
1127
        }
1128
1129
        return implode(', ', $parameters);
1130
    }
1131
1132
    /**
1133
     * @param string $className
1134
     *
1135
     * @return array
1136
     */
1137
    public function getClassMethods($className)
1138
    {
1139
        $class   = new ReflectionClass($className);
1140
        $methods = [];
1141
1142
        foreach ($class->getMethods() as $method) {
1143
            if ($method->isPublic() || $method->isAbstract()) {
1144
                $methods[] = $method->getName();
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
1145
            }
1146
        }
1147
1148
        return $methods;
1149
    }
1150
1151
    /**
1152
     * @param string $template
1153
     *
1154
     * @return Text_Template
1155
     */
1156
    private function getTemplate($template)
1157
    {
1158
        $filename = __DIR__ . DIRECTORY_SEPARATOR . 'Generator' . DIRECTORY_SEPARATOR . $template;
1159
1160
        if (!isset(self::$templates[$filename])) {
1161
            self::$templates[$filename] = new Text_Template($filename);
1162
        }
1163
1164
        return self::$templates[$filename];
1165
    }
1166
}
1167