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

generateMockedMethodDefinition()   C

Complexity

Conditions 11
Paths 12

Size

Total Lines 58
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 34
nc 12
nop 12
dl 0
loc 58
rs 6.4179
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 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