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

MocksEntityManager::ormAttributesToData()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 4
c 3
b 0
f 0
dl 0
loc 9
rs 10
cc 2
nc 2
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
     * 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 EntityManager $em
50
     * @return m\Mock|Entity
51
     */
52
    public function ormCreateMockedEntity($class, $data = [], $em = null)
53
    {
54
        $em = $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
        EntityFetcherMock::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\EntityFetcherMock::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
        EntityFetcherMock::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
        EntityFetcherMock::reset();
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 EntityManager $em The EntityManager that will fetch the class
121
     * @param Entity ...$entities The entities that will be returned
122
     * @return EntityFetcherMock\Result
123
     * @codeCoverageIgnore trivial code
124
     */
125
    public function ormAddResult($class, $em = null, Entity ...$entities)
126
    {
127
        /** @var EntityManager|m\Mock $em */
128
        $em = $em ?: EntityManager::getInstance($class);
129
        return EntityFetcherMock::addResult($class, $em, ...$entities);
0 ignored issues
show
Bug introduced by
It seems like $em can also be of type Mockery\Mock; however, parameter $em of ORM\Testing\EntityFetcherMock::addResult() does only seem to accept ORM\EntityManager, maybe add an additional type check? ( Ignorable by Annotation )

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

129
        return EntityFetcherMock::addResult($class, /** @scrutinizer ignore-type */ $em, ...$entities);
Loading history...
130
    }
131
132
    /**
133
     * Expect fetch for $class
134
     *
135
     * Mocks and expects an EntityFetcher with $entities as result.
136
     *
137
     * @param string        $class    The class that should be fetched
138
     * @param array         $entities The entities that get returned from fetcher
139
     * @param EntityManager $em
140
     * @return m\Mock|EntityFetcher
141
     * @deprecated use $em->shouldReceive('fetch')->once()->passthru()
142
     */
143
    public function ormExpectFetch($class, $entities = [], $em = null)
144
    {
145
        /** @var m\Mock|EntityFetcher $fetcher */
146
        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

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

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

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