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

MocksEntityManager   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 347
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 24
eloc 96
c 3
b 0
f 0
dl 0
loc 347
rs 10

12 Methods

Rating   Name   Duplication   Size   Complexity  
A ormAttributesToData() 0 9 2
A ormExpectDelete() 0 4 1
A ormAllowDelete() 0 21 4
A ormAllowUpdate() 0 35 3
A ormExpectInsert() 0 4 1
A ormAddResult() 0 5 1
A ormExpectFetch() 0 6 1
A ormAllowFetch() 0 15 2
A ormInitMock() 0 18 1
A ormExpectUpdate() 0 4 1
A ormCreateMockedEntity() 0 13 2
A ormAllowInsert() 0 38 5
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
     * @param EntityManagerMock $em
50
     * @return m\Mock|Entity
51
     */
52
    public function ormCreateMockedEntity($class, $data = [], $em = null)
53
    {
54
        /** @var EntityManagerMock $em */
55
        $em = $em ?: EntityManager::getInstance($class);
0 ignored issues
show
introduced by
$em is of type ORM\Testing\EntityManagerMock, thus it always evaluated to true.
Loading history...
56
57
        /** @var Entity|m\MockInterface $entity */
58
        $entity = m::mock($class)->makePartial();
59
        $entity->setEntityManager($em);
60
        $entity->setOriginalData($this->ormAttributesToData($class, $data));
61
        $entity->reset();
62
63
        $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

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

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

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

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

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