ObjectProphecy::checkProphecyMethodsPredictions()   A
last analyzed

Complexity

Conditions 5
Paths 8

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 21
rs 9.2728
c 0
b 0
f 0
cc 5
nc 8
nop 0
1
<?php
2
3
/*
4
 * This file is part of the Prophecy.
5
 * (c) Konstantin Kudryashov <[email protected]>
6
 *     Marcello Duarte <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Prophecy\Prophecy;
13
14
use SebastianBergmann\Comparator\ComparisonFailure;
15
use Prophecy\Comparator\Factory as ComparatorFactory;
16
use Prophecy\Call\Call;
17
use Prophecy\Doubler\LazyDouble;
18
use Prophecy\Argument\ArgumentsWildcard;
19
use Prophecy\Call\CallCenter;
20
use Prophecy\Exception\Prophecy\ObjectProphecyException;
21
use Prophecy\Exception\Prophecy\MethodProphecyException;
22
use Prophecy\Exception\Prediction\AggregateException;
23
use Prophecy\Exception\Prediction\PredictionException;
24
25
/**
26
 * Object prophecy.
27
 *
28
 * @author Konstantin Kudryashov <[email protected]>
29
 */
30
class ObjectProphecy implements ProphecyInterface
31
{
32
    private $lazyDouble;
33
    private $callCenter;
34
    private $revealer;
35
    private $comparatorFactory;
36
37
    /**
38
     * @var MethodProphecy[][]
39
     */
40
    private $methodProphecies = array();
41
42
    /**
43
     * Initializes object prophecy.
44
     *
45
     * @param LazyDouble        $lazyDouble
46
     * @param CallCenter        $callCenter
47
     * @param RevealerInterface $revealer
48
     * @param ComparatorFactory $comparatorFactory
49
     */
50
    public function __construct(
51
        LazyDouble $lazyDouble,
52
        CallCenter $callCenter = null,
53
        RevealerInterface $revealer = null,
54
        ComparatorFactory $comparatorFactory = null
55
    ) {
56
        $this->lazyDouble = $lazyDouble;
57
        $this->callCenter = $callCenter ?: new CallCenter;
58
        $this->revealer   = $revealer ?: new Revealer;
59
60
        $this->comparatorFactory = $comparatorFactory ?: ComparatorFactory::getInstance();
61
    }
62
63
    /**
64
     * Forces double to extend specific class.
65
     *
66
     * @param string $class
67
     *
68
     * @return $this
69
     */
70
    public function willExtend($class)
71
    {
72
        $this->lazyDouble->setParentClass($class);
73
74
        return $this;
75
    }
76
77
    /**
78
     * Forces double to implement specific interface.
79
     *
80
     * @param string $interface
81
     *
82
     * @return $this
83
     */
84
    public function willImplement($interface)
85
    {
86
        $this->lazyDouble->addInterface($interface);
87
88
        return $this;
89
    }
90
91
    /**
92
     * Sets constructor arguments.
93
     *
94
     * @param array $arguments
95
     *
96
     * @return $this
97
     */
98
    public function willBeConstructedWith(array $arguments = null)
99
    {
100
        $this->lazyDouble->setArguments($arguments);
101
102
        return $this;
103
    }
104
105
    /**
106
     * Reveals double.
107
     *
108
     * @return object
109
     *
110
     * @throws \Prophecy\Exception\Prophecy\ObjectProphecyException If double doesn't implement needed interface
111
     */
112
    public function reveal()
113
    {
114
        $double = $this->lazyDouble->getInstance();
115
116
        if (null === $double || !$double instanceof ProphecySubjectInterface) {
117
            throw new ObjectProphecyException(
118
                "Generated double must implement ProphecySubjectInterface, but it does not.\n".
119
                'It seems you have wrongly configured doubler without required ClassPatch.',
120
                $this
121
            );
122
        }
123
124
        $double->setProphecy($this);
125
126
        return $double;
127
    }
128
129
    /**
130
     * Adds method prophecy to object prophecy.
131
     *
132
     * @param MethodProphecy $methodProphecy
133
     *
134
     * @throws \Prophecy\Exception\Prophecy\MethodProphecyException If method prophecy doesn't
135
     *                                                              have arguments wildcard
136
     */
137
    public function addMethodProphecy(MethodProphecy $methodProphecy)
138
    {
139
        $argumentsWildcard = $methodProphecy->getArgumentsWildcard();
140
        if (null === $argumentsWildcard) {
141
            throw new MethodProphecyException(sprintf(
142
                "Can not add prophecy for a method `%s::%s()`\n".
143
                "as you did not specify arguments wildcard for it.",
144
                get_class($this->reveal()),
145
                $methodProphecy->getMethodName()
146
            ), $methodProphecy);
147
        }
148
149
        $methodName = strtolower($methodProphecy->getMethodName());
150
151
        if (!isset($this->methodProphecies[$methodName])) {
152
            $this->methodProphecies[$methodName] = array();
153
        }
154
155
        $this->methodProphecies[$methodName][] = $methodProphecy;
156
    }
157
158
    /**
159
     * Returns either all or related to single method prophecies.
160
     *
161
     * @param null|string $methodName
162
     *
163
     * @return MethodProphecy[]
164
     */
165
    public function getMethodProphecies($methodName = null)
166
    {
167
        if (null === $methodName) {
168
            return $this->methodProphecies;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->methodProphecies; (Prophecy\Prophecy\MethodProphecy[][]) is incompatible with the return type documented by Prophecy\Prophecy\Object...cy::getMethodProphecies of type Prophecy\Prophecy\MethodProphecy[].

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
169
        }
170
171
        $methodName = strtolower($methodName);
172
173
        if (!isset($this->methodProphecies[$methodName])) {
174
            return array();
175
        }
176
177
        return $this->methodProphecies[$methodName];
178
    }
179
180
    /**
181
     * Makes specific method call.
182
     *
183
     * @param string $methodName
184
     * @param array  $arguments
185
     *
186
     * @return mixed
187
     */
188
    public function makeProphecyMethodCall($methodName, array $arguments)
189
    {
190
        $arguments = $this->revealer->reveal($arguments);
191
        $return    = $this->callCenter->makeCall($this, $methodName, $arguments);
192
193
        return $this->revealer->reveal($return);
194
    }
195
196
    /**
197
     * Finds calls by method name & arguments wildcard.
198
     *
199
     * @param string            $methodName
200
     * @param ArgumentsWildcard $wildcard
201
     *
202
     * @return Call[]
203
     */
204
    public function findProphecyMethodCalls($methodName, ArgumentsWildcard $wildcard)
205
    {
206
        return $this->callCenter->findCalls($methodName, $wildcard);
207
    }
208
209
    /**
210
     * Checks that registered method predictions do not fail.
211
     *
212
     * @throws \Prophecy\Exception\Prediction\AggregateException If any of registered predictions fail
213
     * @throws \Prophecy\Exception\Call\UnexpectedCallException
214
     */
215
    public function checkProphecyMethodsPredictions()
216
    {
217
        $exception = new AggregateException(sprintf("%s:\n", get_class($this->reveal())));
218
        $exception->setObjectProphecy($this);
219
220
        $this->callCenter->checkUnexpectedCalls();
221
222
        foreach ($this->methodProphecies as $prophecies) {
223
            foreach ($prophecies as $prophecy) {
224
                try {
225
                    $prophecy->checkPrediction();
226
                } catch (PredictionException $e) {
227
                    $exception->append($e);
228
                }
229
            }
230
        }
231
232
        if (count($exception->getExceptions())) {
233
            throw $exception;
234
        }
235
    }
236
237
    /**
238
     * Creates new method prophecy using specified method name and arguments.
239
     *
240
     * @param string $methodName
241
     * @param array  $arguments
242
     *
243
     * @return MethodProphecy
244
     */
245
    public function __call($methodName, array $arguments)
246
    {
247
        $arguments = new ArgumentsWildcard($this->revealer->reveal($arguments));
248
249
        foreach ($this->getMethodProphecies($methodName) as $prophecy) {
250
            $argumentsWildcard = $prophecy->getArgumentsWildcard();
251
            $comparator = $this->comparatorFactory->getComparatorFor(
252
                $argumentsWildcard, $arguments
253
            );
254
255
            try {
256
                $comparator->assertEquals($argumentsWildcard, $arguments);
257
                return $prophecy;
258
            } catch (ComparisonFailure $failure) {}
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
259
        }
260
261
        return new MethodProphecy($this, $methodName, $arguments);
262
    }
263
264
    /**
265
     * Tries to get property value from double.
266
     *
267
     * @param string $name
268
     *
269
     * @return mixed
270
     */
271
    public function __get($name)
272
    {
273
        return $this->reveal()->$name;
274
    }
275
276
    /**
277
     * Tries to set property value to double.
278
     *
279
     * @param string $name
280
     * @param mixed  $value
281
     */
282
    public function __set($name, $value)
283
    {
284
        $this->reveal()->$name = $this->revealer->reveal($value);
285
    }
286
}
287