Passed
Push — master ( a8d0dc...f605de )
by Thomas
01:38
created

MocksEntityManager::ormInitMock()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 11
c 1
b 0
f 0
dl 0
loc 18
rs 9.9
cc 1
nc 1
nop 2
1
<?php /** @noinspection PhpDocMissingThrowsInspection */
2
3
namespace ORM\Testing;
4
5
use Mockery as m;
6
use ORM\Entity;
7
use ORM\EntityFetcher;
8
use ORM\EntityManager;
9
use ORM\Exception\IncompletePrimaryKey;
10
use PDO;
11
12
/**
13
 * A trait to mock ORM\EntityManager
14
 *
15
 * @package ORM\Testing
16
 * @author  Thomas Flori <[email protected]>
17
 */
18
trait MocksEntityManager
19
{
20
    /**
21
     * Initialize an EntityManager mock object
22
     *
23
     * The mock is partial and you can map and act with it as usual. You should overwrite your dependency injector
24
     * with the returned mock object. You can also call `defineFor*()` on this mock to use this mock for specific
25
     * classes.
26
     *
27
     * The PDO object is mocked too. This object should not receive any calls except for quoting. By default it
28
     * accepts `quote(string)`, `setAttribute(*)` and `getAttribute(ATTR_DRIVER_NAME)`. To retrieve and expect other
29
     * calls you can use `getConnection()` from EntityManager mock object.
30
     *
31
     * @param array $options Options passed to EntityManager constructor
32
     * @param string $driver Database driver you are using (results in different dbal instance)
33
     * @return m\Mock|EntityManager
34
     */
35
    public function ormInitMock($options = [], $driver = 'mysql')
36
    {
37
        /** @var EntityManager|m\Mock $em */
38
        $em = m::mock(EntityManager::class)->makePartial();
39
        $em->__construct($options);
40
        /** @var PDO|m\Mock $pdo */
41
        $pdo = m::mock(PDO::class);
42
43
        $pdo->shouldReceive('setAttribute')->andReturn(true)->byDefault();
44
        $pdo->shouldReceive('getAttribute')->with(PDO::ATTR_DRIVER_NAME)->andReturn($driver)->byDefault();
45
        $pdo->shouldReceive('quote')->with(m::type('string'))->andReturnUsing(
46
            function ($str) {
47
                return '\'' . addcslashes($str, '\'') . '\'';
48
            }
49
        )->byDefault();
50
51
        $em->setConnection($pdo);
52
        return $em;
53
    }
54
55
    /**
56
     * Convert an array with $attributes as keys to an array of columns for $class
57
     *
58
     * e. g. : `assertSame(['first_name' => 'John'], ormAttributesToArray(User::class, ['firstName' => 'John'])`
59
     *
60
     * *Note: this method is idempotent*
61
     *
62
     * @param string $class
63
     * @param array  $attributes
64
     * @return array
65
     */
66
    public function ormAttributesToData($class, array $attributes)
67
    {
68
        $data = [];
69
70
        foreach ($attributes as $attribute => $value) {
71
            $data[call_user_func([$class, 'getColumnName'], $attribute)] = $value;
72
        }
73
74
        return $data;
75
    }
76
77
    /**
78
     * Create a partial mock of Entity $class
79
     *
80
     * @param string        $class
81
     * @param array         $data
82
     * @param EntityManager $em
83
     * @return m\Mock|Entity
84
     */
85
    public function ormCreateMockedEntity($class, $data = [], $em = null)
86
    {
87
        $em = $em ?: EntityManager::getInstance($class);
88
89
        /** @var Entity|m\Mock $entity */
90
        $entity = m::mock($class)->makePartial();
91
        $entity->setEntityManager($em);
92
        $entity->setOriginalData($this->ormAttributesToData($class, $data));
93
        $entity->reset();
94
95
        try {
96
            /** @scrutinizer ignore-type */
97
            $em->map($entity, true, $class);
0 ignored issues
show
Bug introduced by
$entity of type Mockery\Mock is incompatible with the type ORM\Entity expected by parameter $entity of ORM\EntityManager::map(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

97
            $em->map(/** @scrutinizer ignore-type */ $entity, true, $class);
Loading history...
98
        } catch (IncompletePrimaryKey $ex) {
99
            // we tried to map but ignore primary key missing
100
        }
101
        return $entity;
102
    }
103
104
    /**
105
     * Expect an insert for $class
106
     *
107
     * Mocks and expects the calls to sync and insert as they came for `save()` method for a new Entity.
108
     *
109
     * If you omit the auto incremented id in defaultValues it is set to a random value between 1 and 2147483647.
110
     *
111
     * The EntityManager gets determined the same way as in Entity and can be overwritten by third parameter here.
112
     *
113
     * @param string        $class         The class that should get created
114
     * @param array         $defaultValues The default values that came from database (for example: the created column
115
     *                                     has by the default the current timestamp; the id is auto incremented...)
116
     * @param EntityManager $em
117
     */
118
    public function ormExpectInsert($class, $defaultValues = [], $em = null)
119
    {
120
        $expectation = $this->ormAllowInsert($class, $defaultValues, $em);
121
        $expectation->once();
122
    }
123
124
    /**
125
     * Allow an insert for $class
126
     *
127
     * Mocks the calls to sync and insert as they came for `save()` method for a new Entity.
128
     *
129
     * If you omit the auto incremented id in defaultValues it is set to a random value between 1 and 2147483647.
130
     *
131
     * The EntityManager gets determined the same way as in Entity and can be overwritten by third parameter here.
132
     *
133
     * @param string        $class         The class that should get created
134
     * @param array         $defaultValues The default values that came from database (for example: the created column
135
     *                                     has by the default the current timestamp; the id is auto incremented...)
136
     * @param EntityManager $em
137
     * @return m\Expectation
138
     */
139
    public function ormAllowInsert($class, $defaultValues = [], $em = null)
140
    {
141
        /** @var EntityManager|m\Mock $em */
142
        $em = $em ?: EntityManager::getInstance($class);
143
144
        /** @scrutinizer ignore-call */
145
        $expectation = $em->shouldReceive('sync')->with(m::type($class))
0 ignored issues
show
Bug introduced by
The method shouldReceive() does not exist on ORM\EntityManager. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

145
        $expectation = $em->/** @scrutinizer ignore-call */ shouldReceive('sync')->with(m::type($class))

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
146
            ->andReturnUsing(
147
                function (Entity $entity) use ($class, $defaultValues, $em) {
148
                    /** @scrutinizer ignore-call */
149
                    $expectation = $em->shouldReceive('insert')->once()
150
                        ->andReturnUsing(
151
                            function (Entity $entity, $useAutoIncrement = true) use ($class, $defaultValues, $em) {
152
                                if ($useAutoIncrement && !isset($defaultValues[$entity::getPrimaryKeyVars()[0]])) {
153
                                    $defaultValues[$entity::getPrimaryKeyVars()[0]] = mt_rand(1, pow(2, 31) - 1);
154
                                }
155
                                $entity->setOriginalData(array_merge(
156
                                    $this->ormAttributesToData($class, $defaultValues),
157
                                    $entity->getData()
158
                                ));
159
                                $entity->reset();
160
                                $em->map($entity);
161
                                return true;
162
                            }
163
                        );
164
165
                    try {
166
                        $entity->getPrimaryKey();
167
                        $expectation->with(m::type($class), false);
168
                        return false;
169
                    } catch (IncompletePrimaryKey $ex) {
170
                        $expectation->with(m::type($class));
171
                        throw $ex;
172
                    }
173
                }
174
            );
175
176
        return $expectation;
177
    }
178
179
    /**
180
     * Expect fetch for $class
181
     *
182
     * Mocks and expects an EntityFetcher with $entities as result.
183
     *
184
     * @param string        $class    The class that should be fetched
185
     * @param array         $entities The entities that get returned from fetcher
186
     * @param EntityManager $em
187
     * @return m\Mock|EntityFetcher
188
     */
189
    public function ormExpectFetch($class, $entities = [], $em = null)
190
    {
191
        /**  */
192
        list($expectation, $fetcher) = $this->ormAllowFetch($class, $entities, $em);
193
        $expectation->once();
194
        return $fetcher;
195
    }
196
197
    /**
198
     * Allow fetch for $class
199
     *
200
     * Mocks an EntityFetcher with $entities as result.
201
     *
202
     * Returns the Expectation for fetch on entityManager and the mocked EntityFetcher
203
     *
204
     * @param string        $class    The class that should be fetched
205
     * @param array         $entities The entities that get returned from fetcher
206
     * @param EntityManager $em
207
     * @return m\Expectation[]|EntityFetcher[]|m\Mock[]
208
     */
209
    public function ormAllowFetch($class, $entities = [], $em = null)
210
    {
211
        /** @var EntityManager|m\Mock $em */
212
        $em = $em ?: EntityManager::getInstance($class);
213
214
        /** @var m\Mock|EntityFetcher $fetcher */
215
        $fetcher = m::mock(EntityFetcher::class, [ $em, $class ])->makePartial();
216
        $expectation = $em->shouldReceive('fetch')->with($class)->andReturn($fetcher);
217
218
        /** @scrutinizer ignore-call */
219
        $fetcher->shouldReceive('count')->with()->andReturn(count($entities))->byDefault();
220
        array_push($entities, null);
221
        $fetcher->shouldReceive('one')->with()->andReturnValues($entities)->byDefault();
222
223
        return [$expectation, $fetcher];
224
    }
225
226
    /**
227
     * Expect save on $entity
228
     *
229
     * Entity has to be a mock use `emCreateMockedEntity()` to create it.
230
     *
231
     * @param Entity|m\Mock $entity
232
     * @param array  $changingData Emulate changing data during update statement (triggers etc)
233
     * @param array  $updatedData  Emulate data changes in database
234
     */
235
    public function ormExpectUpdate(Entity $entity, $changingData = [], $updatedData = [])
236
    {
237
        $expectation = $this->ormAllowUpdate($entity, $changingData, $updatedData);
238
        $expectation->once();
239
    }
240
241
    /**
242
     * Allow save on $entity
243
     *
244
     * Entity has to be a mock use `emCreateMockedEntity()` to create it.
245
     *
246
     * @param Entity|m\Mock $entity
247
     * @param array $changingData Emulate changing data during update statement (triggers etc)
248
     * @param array $updatedData Emulate data changes in database
249
     * @return m\Expectation
250
     */
251
    public function ormAllowUpdate(Entity $entity, $changingData = [], $updatedData = [])
252
    {
253
        /** @scrutinizer ignore-call */
254
        $expectation = $entity->shouldReceive('save')->andReturnUsing(
0 ignored issues
show
Bug introduced by
The method shouldReceive() does not exist on ORM\Entity. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

254
        $expectation = $entity->/** @scrutinizer ignore-call */ shouldReceive('save')->andReturnUsing(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
255
            function () use ($entity, $updatedData, $changingData) {
256
                $class = get_class($entity);
257
                // sync with database using $updatedData
258
                if (!empty($updatedData)) {
259
                    $newData = $entity->getData();
260
                    $entity->reset();
261
                    $entity->setOriginalData(array_merge(
262
                        $entity->getData(),
263
                        $this->ormAttributesToData($class, $updatedData)
264
                    ));
265
                    $entity->fill($newData);
266
                }
267
268
                if (!$entity->isDirty()) {
269
                    return $entity;
270
                }
271
272
                // update the entity using $changingData
273
                $entity->preUpdate();
274
                $entity->setOriginalData(array_merge(
275
                    $entity->getData(),
276
                    $this->ormAttributesToData($class, $changingData)
277
                ));
278
                $entity->reset();
279
                $entity->postUpdate();
280
281
                return $entity;
282
            }
283
        );
284
285
        return $expectation;
286
    }
287
288
    /**
289
     * Expect delete on $em
290
     *
291
     * If $em is not given it is determined by get_class($entity).
292
     *
293
     * If $entity is a string then it is assumed to be a class name.
294
     *
295
     * @param string|Entity $entity
296
     * @param EntityManager $em
297
     */
298
    public function ormExpectDelete($entity, $em = null)
299
    {
300
        $expectation = $this->ormAllowDelete($entity, $em);
301
        $expectation->once();
302
    }
303
304
    /**
305
     * Allow delete on $em
306
     *
307
     * If $em is not given it is determined by get_class($entity).
308
     *
309
     * If $entity is a string then it is assumed to be a class name.
310
     *
311
     * @param string|Entity $entity
312
     * @param EntityManager $em
313
     * @return m\Expectation
314
     */
315
    public function ormAllowDelete($entity, $em = null)
316
    {
317
        $class = is_string($entity) ? $entity : get_class($entity);
318
319
        /** @var EntityManager|m\Mock $em */
320
        $em = $em ?: EntityManager::getInstance($class);
321
322
        $expectation = $em->shouldReceive('delete');
323
        if (is_string($entity)) {
324
            $expectation->with(m::type($class));
325
        } else {
326
            $expectation->with($entity);
327
        }
328
        $expectation->once()->andReturnUsing(
329
            function (Entity $entity) {
330
                $entity->setOriginalData([]);
331
                return true;
332
            }
333
        );
334
335
        return $expectation;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $expectation returns the type Mockery\HigherOrderMessage which is incompatible with the documented return type Mockery\Expectation.
Loading history...
336
    }
337
}
338