1<?php
2
3/*
4 * This file is part of the Prophecy.
5 * (c) Konstantin Kudryashov <ever.zet@gmail.com>
6 *     Marcello Duarte <marcello.duarte@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Prophecy\Prophecy;
13
14use SebastianBergmann\Comparator\ComparisonFailure;
15use Prophecy\Comparator\Factory as ComparatorFactory;
16use Prophecy\Call\Call;
17use Prophecy\Doubler\LazyDouble;
18use Prophecy\Argument\ArgumentsWildcard;
19use Prophecy\Call\CallCenter;
20use Prophecy\Exception\Prophecy\ObjectProphecyException;
21use Prophecy\Exception\Prophecy\MethodProphecyException;
22use Prophecy\Exception\Prediction\AggregateException;
23use Prophecy\Exception\Prediction\PredictionException;
24
25/**
26 * Object prophecy.
27 *
28 * @author Konstantin Kudryashov <ever.zet@gmail.com>
29 */
30class 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 = $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;
169        }
170
171        if (!isset($this->methodProphecies[$methodName])) {
172            return array();
173        }
174
175        return $this->methodProphecies[$methodName];
176    }
177
178    /**
179     * Makes specific method call.
180     *
181     * @param string $methodName
182     * @param array  $arguments
183     *
184     * @return mixed
185     */
186    public function makeProphecyMethodCall($methodName, array $arguments)
187    {
188        $arguments = $this->revealer->reveal($arguments);
189        $return    = $this->callCenter->makeCall($this, $methodName, $arguments);
190
191        return $this->revealer->reveal($return);
192    }
193
194    /**
195     * Finds calls by method name & arguments wildcard.
196     *
197     * @param string            $methodName
198     * @param ArgumentsWildcard $wildcard
199     *
200     * @return Call[]
201     */
202    public function findProphecyMethodCalls($methodName, ArgumentsWildcard $wildcard)
203    {
204        return $this->callCenter->findCalls($methodName, $wildcard);
205    }
206
207    /**
208     * Checks that registered method predictions do not fail.
209     *
210     * @throws \Prophecy\Exception\Prediction\AggregateException If any of registered predictions fail
211     */
212    public function checkProphecyMethodsPredictions()
213    {
214        $exception = new AggregateException(sprintf("%s:\n", get_class($this->reveal())));
215        $exception->setObjectProphecy($this);
216
217        foreach ($this->methodProphecies as $prophecies) {
218            foreach ($prophecies as $prophecy) {
219                try {
220                    $prophecy->checkPrediction();
221                } catch (PredictionException $e) {
222                    $exception->append($e);
223                }
224            }
225        }
226
227        if (count($exception->getExceptions())) {
228            throw $exception;
229        }
230    }
231
232    /**
233     * Creates new method prophecy using specified method name and arguments.
234     *
235     * @param string $methodName
236     * @param array  $arguments
237     *
238     * @return MethodProphecy
239     */
240    public function __call($methodName, array $arguments)
241    {
242        $arguments = new ArgumentsWildcard($this->revealer->reveal($arguments));
243
244        foreach ($this->getMethodProphecies($methodName) as $prophecy) {
245            $argumentsWildcard = $prophecy->getArgumentsWildcard();
246            $comparator = $this->comparatorFactory->getComparatorFor(
247                $argumentsWildcard, $arguments
248            );
249
250            try {
251                $comparator->assertEquals($argumentsWildcard, $arguments);
252                return $prophecy;
253            } catch (ComparisonFailure $failure) {}
254        }
255
256        return new MethodProphecy($this, $methodName, $arguments);
257    }
258
259    /**
260     * Tries to get property value from double.
261     *
262     * @param string $name
263     *
264     * @return mixed
265     */
266    public function __get($name)
267    {
268        return $this->reveal()->$name;
269    }
270
271    /**
272     * Tries to set property value to double.
273     *
274     * @param string $name
275     * @param mixed  $value
276     */
277    public function __set($name, $value)
278    {
279        $this->reveal()->$name = $this->revealer->reveal($value);
280    }
281}
282