Completed
Pull Request — master (#634)
by
unknown
02:52
created

Container::rememberMock()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 13
ccs 6
cts 6
cp 1
rs 9.4285
cc 2
eloc 6
nc 2
nop 1
crap 2
1
<?php
2
/**
3
 * Mockery
4
 *
5
 * LICENSE
6
 *
7
 * This source file is subject to the new BSD license that is bundled
8
 * with this package in the file LICENSE.txt.
9
 * It is also available through the world-wide-web at this URL:
10
 * http://github.com/padraic/mockery/blob/master/LICENSE
11
 * If you did not receive a copy of the license and are unable to
12
 * obtain it through the world-wide-web, please send an email
13
 * to [email protected] so we can send you a copy immediately.
14
 *
15
 * @category   Mockery
16
 * @package    Mockery
17
 * @copyright  Copyright (c) 2010 Pádraic Brady (http://blog.astrumfutura.com)
18
 * @license    http://github.com/padraic/mockery/blob/master/LICENSE New BSD License
19
 */
20
21
namespace Mockery;
22
23
use Mockery\Generator\Generator;
24
use Mockery\Generator\MockConfigurationBuilder;
25
use Mockery\Loader\Loader as LoaderInterface;
26
27
class Container
28
{
29
    const BLOCKS = \Mockery::BLOCKS;
30
31
    /**
32
     * Store of mock objects
33
     *
34
     * @var array
35
     */
36
    protected $_mocks = array();
37
38
    /**
39
     * Order number of allocation
40
     *
41
     * @var int
42
     */
43
    protected $_allocatedOrder = 0;
44
45
    /**
46
     * Current ordered number
47
     *
48
     * @var int
49
     */
50
    protected $_currentOrder = 0;
51
52
    /**
53
     * Ordered groups
54
     *
55
     * @var array
56
     */
57
    protected $_groups = array();
58
59
    /**
60
     * @var Generator\Generator
61
     */
62
    protected $_generator;
63
64
    /**
65
     * @var LoaderInterface
66
     */
67
    protected $_loader;
68
69
    /**
70
     * @var array
71
     */
72
    protected $_namedMocks = array();
73
74 420
    public function __construct(Generator $generator = null, LoaderInterface $loader = null)
75
    {
76 420
        $this->_generator = $generator ?: \Mockery::getDefaultGenerator();
77 420
        $this->_loader = $loader ?: \Mockery::getDefaultLoader();
78 420
    }
79
80
    /**
81
     * Generates a new mock object for this container
82
     *
83
     * I apologies in advance for this. A God Method just fits the API which
84
     * doesn't require differentiating between classes, interfaces, abstracts,
85
     * names or partials - just so long as it's something that can be mocked.
86
     * I'll refactor it one day so it's easier to follow.
87
     *
88
     * @throws Exception\RuntimeException
89
     * @throws Exception
90
     * @return \Mockery\Mock
91
     */
92 419
    public function mock()
93
    {
94 419
        $expectationClosure = null;
95 419
        $quickdefs = array();
96 419
        $constructorArgs = null;
97 419
        $blocks = array();
98 419
        $args = func_get_args();
99
100 419
        if (count($args) > 1) {
101 25
            $finalArg = end($args);
102 25
            reset($args);
103 25
            if (is_callable($finalArg) && is_object($finalArg)) {
104 1
                $expectationClosure = array_pop($args);
105 1
            }
106 25
        }
107
108 419
        $builder = new MockConfigurationBuilder();
109
110 419
        foreach ($args as $k => $arg) {
111 408
            if ($arg instanceof MockConfigurationBuilder) {
112 5
                $builder = $arg;
113 5
                unset($args[$k]);
114 5
            }
115 419
        }
116 419
        reset($args);
117
118 419
        $builder->setParameterOverrides(\Mockery::getConfiguration()->getInternalClassMethodParamMaps());
119
120 419
        while (count($args) > 0) {
121 406
            $arg = current($args);
122
            // check for multiple interfaces
123 406
            if (is_string($arg) && strpos($arg, ',') && !strpos($arg, ']')) {
124 6
                $interfaces = explode(',', str_replace(' ', '', $arg));
125 6
                foreach ($interfaces as $i) {
126 6
                    if (!interface_exists($i, true) && !class_exists($i, true)) {
127 1
                        throw new \Mockery\Exception(
128
                            'Class name follows the format for defining multiple'
129
                            . ' interfaces, however one or more of the interfaces'
130 1
                            . ' do not exist or are not included, or the base class'
131 1
                            . ' (which you may omit from the mock definition) does not exist'
132 1
                        );
133
                    }
134 5
                }
135 5
                $builder->addTargets($interfaces);
136 5
                array_shift($args);
137
138 5
                continue;
139 401
            } elseif (is_string($arg) && substr($arg, 0, 6) == 'alias:') {
140 4
                $name = array_shift($args);
141 4
                $name = str_replace('alias:', '', $name);
142 4
                $builder->addTarget('stdClass');
143 4
                $builder->setName($name);
144 4
                continue;
145 397
            } elseif (is_string($arg) && substr($arg, 0, 9) == 'overload:') {
146 11
                $name = array_shift($args);
147 11
                $name = str_replace('overload:', '', $name);
148 11
                $builder->setInstanceMock(true);
149 11
                $builder->addTarget('stdClass');
150 11
                $builder->setName($name);
151 11
                continue;
152 386
            } elseif (is_string($arg) && substr($arg, strlen($arg)-1, 1) == ']') {
153 17
                $parts = explode('[', $arg);
154 17
                if (!class_exists($parts[0], true) && !interface_exists($parts[0], true)) {
155 1
                    throw new \Mockery\Exception('Can only create a partial mock from'
156 1
                    . ' an existing class or interface');
157
                }
158 17
                $class = $parts[0];
159 16
                $parts[1] = str_replace(' ', '', $parts[1]);
160 16
                $partialMethods = explode(',', strtolower(rtrim($parts[1], ']')));
161 16
                $builder->addTarget($class);
162 16
                $builder->setWhiteListedMethods($partialMethods);
163 16
                array_shift($args);
164 16
                continue;
165 375
            } elseif (is_string($arg) && (class_exists($arg, true) || interface_exists($arg, true))) {
166 334
                $class = array_shift($args);
167 334
                $builder->addTarget($class);
168 334
                continue;
169 63
            } elseif (is_string($arg) && !\Mockery::getConfiguration()->mockingNonExistentMethodsAllowed() && (!class_exists($arg, true) && !interface_exists($arg, true))) {
170 1
                throw new \Mockery\Exception("Mockery can't find '$arg' so can't mock it");
171 62
            } elseif (is_string($arg)) {
172 17
                if (!$this->isValidClassName($arg)) {
173 1
                    throw new \Mockery\Exception('Class name contains invalid characters');
174
                }
175 16
                $class = array_shift($args);
176 16
                $builder->addTarget($class);
177 16
                continue;
178 46
            } elseif (is_object($arg)) {
179 24
                $partial = array_shift($args);
180 24
                $builder->addTarget($partial);
181 24
                continue;
182 24
            } elseif (is_array($arg) && !empty($arg) && array_keys($arg) !== range(0, count($arg) - 1)) {
183
                // if associative array
184 15
                if (array_key_exists(self::BLOCKS, $arg)) {
185 2
                    $blocks = $arg[self::BLOCKS];
186 2
                }
187 15
                unset($arg[self::BLOCKS]);
188 15
                $quickdefs = array_shift($args);
189 15
                continue;
190 10
            } elseif (is_array($arg)) {
191 10
                $constructorArgs = array_shift($args);
192 10
                continue;
193
            }
194
195
            throw new \Mockery\Exception(
196
                'Unable to parse arguments sent to '
197
                . get_class($this) . '::mock()'
198
            );
199
        }
200
201 416
        $builder->addBlackListedMethods($blocks);
202
203 416
        if (defined('HHVM_VERSION')
204 416
            && isset($class)
205 416
            && ($class === 'Exception' || is_subclass_of($class, 'Exception'))) {
206
            $builder->addBlackListedMethod("setTraceOptions");
207
            $builder->addBlackListedMethod("getTraceOptions");
208
        }
209
210 416
        if (!is_null($constructorArgs)) {
211 10
            $builder->addBlackListedMethod("__construct"); // we need to pass through
212 10
        } else {
213 406
            $builder->setMockOriginalDestructor(true);
214
        }
215
216 416
        if (!empty($partialMethods) && $constructorArgs === null) {
217 11
            $constructorArgs = array();
218 11
        }
219
220 416
        $config = $builder->getMockConfiguration();
221
222 416
        $this->checkForNamedMockClashes($config);
223
224 416
        $def = $this->getGenerator()->generate($config);
225
226 415
        if (class_exists($def->getClassName(), $attemptAutoload = false)) {
227 25
            $rfc = new \ReflectionClass($def->getClassName());
228 25
            if (!$rfc->implementsInterface("Mockery\MockInterface")) {
229 1
                throw new \Mockery\Exception\RuntimeException("Could not load mock {$def->getClassName()}, class already exists");
230
            }
231 24
        }
232
233 414
        $this->getLoader()->load($def);
234
235 414
        $mock = $this->_getInstance($def->getClassName(), $constructorArgs);
236 414
        $mock->mockery_init($this, $config->getTargetObject());
237
238 414
        if (!empty($quickdefs)) {
239 15
            $mock->shouldReceive($quickdefs)->byDefault();
240 15
        }
241 414
        if (!empty($expectationClosure)) {
242 1
            $expectationClosure($mock);
243 1
        }
244 414
        $this->rememberMock($mock);
245 414
        return $mock;
246
    }
247
248
    public function instanceMock()
249
    {
250
    }
251
252 414
    public function getLoader()
253
    {
254 414
        return $this->_loader;
255
    }
256
257 416
    public function getGenerator()
258
    {
259 416
        return $this->_generator;
260
    }
261
262
    /**
263
     * @param string $method
264
     * @return string|null
265
     */
266 7
    public function getKeyOfDemeterMockFor($method)
267
    {
268 7
        $keys = array_keys($this->_mocks);
269 7
        $match = preg_grep("/__demeter_{$method}$/", $keys);
270 7
        if (count($match) == 1) {
271 6
            $res = array_values($match);
272 6
            if (count($res) > 0) {
273 6
                return $res[0];
274
            }
275
        }
276 1
        return null;
277
    }
278
279
    /**
280
     * @return array
281
     */
282 5
    public function getMocks()
283
    {
284 5
        return $this->_mocks;
285
    }
286
287
    /**
288
     *  Tear down tasks for this container
289
     *
290
     * @throws \Exception
291
     * @return void
292
     */
293 411
    public function mockery_teardown()
294
    {
295
        try {
296 411
            $this->mockery_verify();
297 411
        } catch (\Exception $e) {
298
            $this->mockery_close();
299
            throw $e;
300
        }
301 411
    }
302
303
    /**
304
     * Verify the container mocks
305
     *
306
     * @return void
307
     */
308 411
    public function mockery_verify()
309
    {
310 411
        foreach ($this->_mocks as $mock) {
311 146
            $mock->mockery_verify();
312 411
        }
313 411
    }
314
315
    /**
316
     * Reset the container to its original state
317
     *
318
     * @return void
319
     */
320 418
    public function mockery_close()
321
    {
322 418
        foreach ($this->_mocks as $mock) {
323 398
            $mock->mockery_teardown();
324 418
        }
325 418
        $this->_mocks = array();
326 418
    }
327
328
    /**
329
     * Fetch the next available allocation order number
330
     *
331
     * @return int
332
     */
333 1
    public function mockery_allocateOrder()
334
    {
335 1
        $this->_allocatedOrder += 1;
336 1
        return $this->_allocatedOrder;
337
    }
338
339
    /**
340
     * Set ordering for a group
341
     *
342
     * @param mixed $group
343
     * @param int $order
344
     */
345
    public function mockery_setGroup($group, $order)
346
    {
347
        $this->_groups[$group] = $order;
348
    }
349
350
    /**
351
     * Fetch array of ordered groups
352
     *
353
     * @return array
354
     */
355 1
    public function mockery_getGroups()
356
    {
357 1
        return $this->_groups;
358
    }
359
360
    /**
361
     * Set current ordered number
362
     *
363
     * @param int $order
364
     * @return int The current order number that was set
365
     */
366 137
    public function mockery_setCurrentOrder($order)
367
    {
368 7
        $this->_currentOrder = $order;
369 137
        return $this->_currentOrder;
370
    }
371
372
    /**
373
     * Get current ordered number
374
     *
375
     * @return int
376
     */
377
    public function mockery_getCurrentOrder()
378
    {
379
        return $this->_currentOrder;
380
    }
381
382
    /**
383
     * Validate the current mock's ordering
384
     *
385
     * @param string $method
386
     * @param int $order
387
     * @throws \Mockery\Exception
388
     * @return void
389
     */
390 1
    public function mockery_validateOrder($method, $order, \Mockery\MockInterface $mock)
391
    {
392 1
        if ($order < $this->_currentOrder) {
393 1
            $exception = new \Mockery\Exception\InvalidOrderException(
394 1
                'Method ' . $method . ' called out of order: expected order '
395 1
                . $order . ', was ' . $this->_currentOrder
396 1
            );
397 1
            $exception->setMock($mock)
398 1
                ->setMethodName($method)
399 1
                ->setExpectedOrder($order)
400 1
                ->setActualOrder($this->_currentOrder);
401 1
            throw $exception;
402
        }
403 1
        $this->mockery_setCurrentOrder($order);
404 1
    }
405
406
    /**
407
     * Gets the count of expectations on the mocks
408
     *
409
     * @return int
410
     */
411 411
    public function mockery_getExpectationCount()
412
    {
413 411
        $count = 0;
414 411
        foreach ($this->_mocks as $mock) {
415 29
            $count += $mock->mockery_getExpectationCount();
416 411
        }
417 411
        return $count;
418
    }
419
420
    /**
421
     * Store a mock and set its container reference
422
     *
423
     * @param \Mockery\Mock
424
     * @return \Mockery\Mock
425
     */
426 414
    public function rememberMock(\Mockery\MockInterface $mock)
427
    {
428 414
        if (!isset($this->_mocks[get_class($mock)])) {
429 404
            $this->_mocks[get_class($mock)] = $mock;
430 404
        } else {
431
            /**
432
             * This condition triggers for an instance mock where origin mock
433
             * is already remembered
434
             */
435 21
            $this->_mocks[] = $mock;
436
        }
437 414
        return $mock;
438
    }
439
440
    /**
441
     * Retrieve the last remembered mock object, which is the same as saying
442
     * retrieve the current mock being programmed where you have yet to call
443
     * mock() to change it - thus why the method name is "self" since it will be
444
     * be used during the programming of the same mock.
445
     *
446
     * @return \Mockery\Mock
447
     */
448 2
    public function self()
449
    {
450 2
        $mocks = array_values($this->_mocks);
451 2
        $index = count($mocks) - 1;
452 2
        return $mocks[$index];
453
    }
454
455
    /**
456
     * Return a specific remembered mock according to the array index it
457
     * was stored to in this container instance
458
     *
459
     * @return \Mockery\Mock
460
     */
461 11
    public function fetchMock($reference)
462
    {
463 11
        if (isset($this->_mocks[$reference])) {
464 11
            return $this->_mocks[$reference];
465
        }
466
    }
467
468 414
    protected function _getInstance($mockName, $constructorArgs = null)
469
    {
470 414
        if ($constructorArgs !== null) {
471 21
            $r = new \ReflectionClass($mockName);
472 21
            return $r->newInstanceArgs($constructorArgs);
473
        }
474
475
        try {
476 394
            $instantiator = new Instantiator;
477 394
            $instance = $instantiator->instantiate($mockName);
478 394
        } catch (\Exception $ex) {
479
            $internalMockName = $mockName . '_Internal';
480
481
            if (!class_exists($internalMockName)) {
482
                eval("class $internalMockName extends $mockName {" .
483
                        'public function __construct() {}' .
484
                    '}');
485
            }
486
487
            $instance = new $internalMockName();
488
        }
489
490 394
        return $instance;
491
    }
492
493 416
    protected function checkForNamedMockClashes($config)
494
    {
495 416
        $name = $config->getName();
496
497 416
        if (!$name) {
498 396
            return;
499
        }
500
501 20
        $hash = $config->getHash();
502
503 20
        if (isset($this->_namedMocks[$name])) {
504 1
            if ($hash !== $this->_namedMocks[$name]) {
505 1
                throw new \Mockery\Exception(
506 1
                    "The mock named '$name' has been already defined with a different mock configuration"
507 1
                );
508
            }
509
        }
510
511 20
        $this->_namedMocks[$name] = $hash;
512 20
    }
513
514
    /**
515
     * see http://php.net/manual/en/language.oop5.basic.php
516
     * @param string $className
517
     * @return bool
518
     */
519 22
    public function isValidClassName($className)
520
    {
521 22
        $pos = strpos($className, '\\');
522 22
        if ($pos === 0) {
523 3
            $className = substr($className, 1); // remove the first backslash
524 3
        }
525
        // all the namespaces and class name should match the regex
526 22
        $invalidNames = array_filter(explode('\\', $className), function ($name) {
527 22
            return !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $name);
528 22
        });
529 22
        return empty($invalidNames);
530
    }
531
}
532