Passed
Pull Request — master (#55)
by Thomas
01:33
created

MocksEntityManager   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 340
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 20
eloc 96
c 4
b 0
f 0
dl 0
loc 340
rs 10

12 Methods

Rating   Name   Duplication   Size   Complexity  
A ormAttributesToData() 0 9 2
A ormExpectDelete() 0 4 1
A ormAllowDelete() 0 21 3
A ormAllowUpdate() 0 35 3
A ormExpectInsert() 0 4 1
A ormAddResult() 0 5 1
A ormExpectFetch() 0 6 1
A ormAllowFetch() 0 15 1
A ormInitMock() 0 18 1
A ormExpectUpdate() 0 4 1
A ormCreateMockedEntity() 0 13 1
A ormAllowInsert() 0 38 4
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
     * Convert an array with $attributes as keys to an array of columns for $class
22
     *
23
     * e. g. : `assertSame(['first_name' => 'John'], ormAttributesToArray(User::class, ['firstName' => 'John'])`
24
     *
25
     * *Note: this method is idempotent*
26
     *
27
     * @param string $class
28
     * @param array  $attributes
29
     * @return array
30
     */
31
    public function ormAttributesToData($class, array $attributes)
32
    {
33
        $data = [];
34
35
        foreach ($attributes as $attribute => $value) {
36
            $data[call_user_func([$class, 'getColumnName'], $attribute)] = $value;
37
        }
38
39
        return $data;
40
    }
41
42
    /**
43
     * Create a partial mock of Entity $class
44
     *
45
     * *Note: the entity will get a random primary key if not predefined.*
46
     *
47
     * @param string        $class
48
     * @param array         $data
49
     * @return m\Mock|Entity
50
     */
51
    public function ormCreateMockedEntity($class, $data = [])
52
    {
53
        /** @var EntityManagerMock $em */
54
        $em = EntityManager::getInstance($class);
55
56
        /** @var Entity|m\MockInterface $entity */
57
        $entity = m::mock($class)->makePartial();
58
        $entity->setEntityManager($em);
59
        $entity->setOriginalData($this->ormAttributesToData($class, $data));
60
        $entity->reset();
61
62
        $em->addEntity($entity);
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\Testing\EntityManagerMock::addEntity(). ( Ignorable by Annotation )

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

62
        $em->addEntity(/** @scrutinizer ignore-type */ $entity);
Loading history...
63
        return $entity;
64
    }
65
66
    /**
67
     * Initialize an EntityManager mock object
68
     *
69
     * The mock is partial and you can map and act with it as usual. You should overwrite your dependency injector
70
     * with the returned mock object. You can also call `defineFor*()` on this mock to use this mock for specific
71
     * classes.
72
     *
73
     * The PDO object is mocked too. This object should not receive any calls except for quoting. By default it
74
     * accepts `quote(string)`, `setAttribute(*)` and `getAttribute(ATTR_DRIVER_NAME)`. To retrieve and expect other
75
     * calls you can use `getConnection()` from EntityManager mock object.
76
     *
77
     * @param array $options Options passed to EntityManager constructor
78
     * @param string $driver Database driver you are using (results in different dbal instance)
79
     * @return m\Mock|EntityManager
80
     */
81
    public function ormInitMock($options = [], $driver = 'mysql')
82
    {
83
        /** @var EntityManager|m\Mock $em */
84
        $em = m::mock(EntityManagerMock::class)->makePartial();
85
        $em->__construct($options);
86
87
        /** @var PDO|m\Mock $pdo */
88
        $pdo = m::mock(PDO::class);
89
        $pdo->shouldReceive('setAttribute')->andReturn(true)->byDefault();
90
        $pdo->shouldReceive('getAttribute')->with(PDO::ATTR_DRIVER_NAME)->andReturn($driver)->byDefault();
91
        $pdo->shouldReceive('quote')->with(m::type('string'))->andReturnUsing(
92
            function ($str) {
93
                return '\'' . addcslashes($str, '\'') . '\'';
94
            }
95
        )->byDefault();
96
        $em->setConnection($pdo);
97
98
        return $em;
99
    }
100
101
    /**
102
     * Add a result to EntityFetcher for $class
103
     *
104
     * You can specify the query that you expect in the returned result.
105
     *
106
     * Example:
107
     * ```php
108
     * $this->ormAddResult(Article::class, $em, new Article(['title' => 'Foo']))
109
     *   ->where('deleted_at IS NULL')
110
     *   ->where('title', 'Foo');
111
     *
112
     * $entity = $em->fetch('Article::class')
113
     *   ->where('deleted_at IS NULL')
114
     *   ->where('title', 'Foo')
115
     *   ->one();
116
     * ```
117
     *
118
     * @param string $class The class of an Entity
119
     * @param Entity ...$entities The entities that will be returned
120
     * @return EntityFetcherMock\Result
121
     * @codeCoverageIgnore trivial code
122
     */
123
    public function ormAddResult($class, Entity ...$entities)
124
    {
125
        /** @var EntityManagerMock|m\Mock $em */
126
        $em = EntityManager::getInstance($class);
127
        return $em->addResult($class, ...$entities);
128
    }
129
130
    /**
131
     * Expect fetch for $class
132
     *
133
     * Mocks and expects an EntityFetcher with $entities as result.
134
     *
135
     * @param string        $class    The class that should be fetched
136
     * @param array         $entities The entities that get returned from fetcher
137
     * @return m\Mock|EntityFetcher
138
     * @deprecated use $em->shouldReceive('fetch')->once()->passthru()
139
     */
140
    public function ormExpectFetch($class, $entities = [])
141
    {
142
        /** @var m\Mock|EntityFetcher $fetcher */
143
        list($expectation, $fetcher) = $this->ormAllowFetch($class, $entities);
0 ignored issues
show
Deprecated Code introduced by
The function ORM\Testing\MocksEntityManager::ormAllowFetch() has been deprecated: every fetch is allowed now (change with $em->shouldNotReceive('fetch')) ( Ignorable by Annotation )

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

143
        list($expectation, $fetcher) = /** @scrutinizer ignore-deprecated */ $this->ormAllowFetch($class, $entities);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
144
        $expectation->once();
145
        return $fetcher;
146
    }
147
148
    /**
149
     * Allow fetch for $class
150
     *
151
     * Mocks an EntityFetcher with $entities as result.
152
     *
153
     * Returns the Expectation for fetch on entityManager and the mocked EntityFetcher
154
     *
155
     * @param string        $class    The class that should be fetched
156
     * @param array         $entities The entities that get returned from fetcher
157
     * @return m\Expectation[]|EntityFetcher[]|m\Mock[]
158
     * @deprecated every fetch is allowed now (change with $em->shouldNotReceive('fetch'))
159
     */
160
    public function ormAllowFetch($class, $entities = [])
161
    {
162
        /** @var EntityManager|m\Mock $em */
163
        $em = EntityManager::getInstance($class);
164
165
        /** @var m\Mock|EntityFetcher $fetcher */
166
        $fetcher = m::mock(EntityFetcher::class, [ $em, $class ])->makePartial();
167
        $expectation = $em->shouldReceive('fetch')->with($class)->andReturn($fetcher);
1 ignored issue
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

167
        $expectation = $em->/** @scrutinizer ignore-call */ shouldReceive('fetch')->with($class)->andReturn($fetcher);

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...
168
169
        /** @scrutinizer ignore-call */
170
        $fetcher->shouldReceive('count')->with()->andReturn(count($entities))->byDefault();
171
        array_push($entities, null);
172
        $fetcher->shouldReceive('one')->with()->andReturnValues($entities)->byDefault();
173
174
        return [$expectation, $fetcher];
175
    }
176
177
    /**
178
     * Expect an insert for $class
179
     *
180
     * Mocks and expects the calls to sync and insert as they came for `save()` method for a new Entity.
181
     *
182
     * If you omit the auto incremented id in defaultValues it is set to a random value between 1 and 2147483647.
183
     *
184
     * The EntityManager gets determined the same way as in Entity and can be overwritten by third parameter here.
185
     *
186
     * @param string        $class         The class that should get created
187
     * @param array         $defaultValues The default values that came from database (for example: the created column
188
     *                                     has by the default the current timestamp; the id is auto incremented...)
189
     */
190
    public function ormExpectInsert($class, $defaultValues = [])
191
    {
192
        $expectation = $this->ormAllowInsert($class, $defaultValues);
193
        $expectation->once();
194
    }
195
196
    /**
197
     * Allow an insert for $class
198
     *
199
     * Mocks the calls to sync and insert as they came for `save()` method for a new Entity.
200
     *
201
     * If you omit the auto incremented id in defaultValues it is set to a random value between 1 and 2147483647.
202
     *
203
     * The EntityManager gets determined the same way as in Entity and can be overwritten by third parameter here.
204
     *
205
     * @param string        $class         The class that should get created
206
     * @param array         $defaultValues The default values that came from database (for example: the created column
207
     *                                     has by the default the current timestamp; the id is auto incremented...)
208
     * @return m\Expectation
209
     */
210
    public function ormAllowInsert($class, $defaultValues = [])
211
    {
212
        /** @var EntityManager|m\Mock $em */
213
        $em = EntityManager::getInstance($class);
214
215
        /** @scrutinizer ignore-call */
216
        $expectation = $em->shouldReceive('sync')->with(m::type($class))
217
            ->andReturnUsing(
218
                function (Entity $entity) use ($class, $defaultValues, $em) {
219
                    /** @scrutinizer ignore-call */
220
                    $expectation = $em->shouldReceive('insert')->once()
221
                        ->andReturnUsing(
222
                            function (Entity $entity, $useAutoIncrement = true) use ($class, $defaultValues, $em) {
223
                                if ($useAutoIncrement && !isset($defaultValues[$entity::getPrimaryKeyVars()[0]])) {
224
                                    $defaultValues[$entity::getPrimaryKeyVars()[0]] = mt_rand(1, pow(2, 31) - 1);
225
                                }
226
                                $entity->setOriginalData(array_merge(
227
                                    $this->ormAttributesToData($class, $defaultValues),
228
                                    $entity->getData()
229
                                ));
230
                                $entity->reset();
231
                                $em->map($entity);
232
                                return true;
233
                            }
234
                        );
235
236
                    try {
237
                        $entity->getPrimaryKey();
238
                        $expectation->with(m::type($class), false);
239
                        return false;
240
                    } catch (IncompletePrimaryKey $ex) {
241
                        $expectation->with(m::type($class));
242
                        throw $ex;
243
                    }
244
                }
245
            );
246
247
        return $expectation;
248
    }
249
250
    /**
251
     * Expect save on $entity
252
     *
253
     * Entity has to be a mock use `emCreateMockedEntity()` to create it.
254
     *
255
     * @param Entity|m\Mock $entity
256
     * @param array  $changingData Emulate changing data during update statement (triggers etc)
257
     * @param array  $updatedData  Emulate data changes in database
258
     */
259
    public function ormExpectUpdate(Entity $entity, $changingData = [], $updatedData = [])
260
    {
261
        $expectation = $this->ormAllowUpdate($entity, $changingData, $updatedData);
262
        $expectation->once();
263
    }
264
265
    /**
266
     * Allow save on $entity
267
     *
268
     * Entity has to be a mock use `emCreateMockedEntity()` to create it.
269
     *
270
     * @param Entity|m\Mock $entity
271
     * @param array $changingData Emulate changing data during update statement (triggers etc)
272
     * @param array $updatedData Emulate data changes in database
273
     * @return m\Expectation
274
     */
275
    public function ormAllowUpdate(Entity $entity, $changingData = [], $updatedData = [])
276
    {
277
        /** @scrutinizer ignore-call */
278
        $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

278
        $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...
279
            function () use ($entity, $updatedData, $changingData) {
280
                $class = get_class($entity);
281
                // sync with database using $updatedData
282
                if (!empty($updatedData)) {
283
                    $newData = $entity->getData();
284
                    $entity->reset();
285
                    $entity->setOriginalData(array_merge(
286
                        $entity->getData(),
287
                        $this->ormAttributesToData($class, $updatedData)
288
                    ));
289
                    $entity->fill($newData);
290
                }
291
292
                if (!$entity->isDirty()) {
293
                    return $entity;
294
                }
295
296
                // update the entity using $changingData
297
                $entity->preUpdate();
298
                $entity->setOriginalData(array_merge(
299
                    $entity->getData(),
300
                    $this->ormAttributesToData($class, $changingData)
301
                ));
302
                $entity->reset();
303
                $entity->postUpdate();
304
305
                return $entity;
306
            }
307
        );
308
309
        return $expectation;
310
    }
311
312
    /**
313
     * Expect delete on $em
314
     *
315
     * If $em is not given it is determined by get_class($entity).
316
     *
317
     * If $entity is a string then it is assumed to be a class name.
318
     *
319
     * @param string|Entity $entity
320
     */
321
    public function ormExpectDelete($entity)
322
    {
323
        $expectation = $this->ormAllowDelete($entity);
324
        $expectation->once();
325
    }
326
327
    /**
328
     * Allow delete on $em
329
     *
330
     * If $em is not given it is determined by get_class($entity).
331
     *
332
     * If $entity is a string then it is assumed to be a class name.
333
     *
334
     * @param string|Entity $entity
335
     * @return m\Expectation
336
     */
337
    public function ormAllowDelete($entity)
338
    {
339
        $class = is_string($entity) ? $entity : get_class($entity);
340
341
        /** @var EntityManager|m\Mock $em */
342
        $em = EntityManager::getInstance($class);
343
344
        $expectation = $em->shouldReceive('delete');
345
        if (is_string($entity)) {
346
            $expectation->with(m::type($class));
347
        } else {
348
            $expectation->with($entity);
349
        }
350
        $expectation->once()->andReturnUsing(
351
            function (Entity $entity) {
352
                $entity->setOriginalData([]);
353
                return true;
354
            }
355
        );
356
357
        return $expectation;
358
    }
359
}
360