Passed
Pull Request — master (#55)
by Thomas
02:46
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
     * Get the EntityManagerMock for $class
22
     *
23
     * @param $class
24
     * @return EntityManagerMock|m\MockInterface|EntityManager
25
     * @codeCoverageIgnore proxy method
26
     */
27
    public function ormGetEntityManagerInstance($class)
28
    {
29
        return EntityManager::getInstance($class);
30
    }
31
32
    /**
33
     * Convert an array with $attributes as keys to an array of columns for $class
34
     *
35
     * e. g. : `assertSame(['first_name' => 'John'], ormAttributesToArray(User::class, ['firstName' => 'John'])`
36
     *
37
     * *Note: this method is idempotent*
38
     *
39
     * @param string $class
40
     * @param array  $attributes
41
     * @return array
42
     */
43
    public function ormAttributesToData($class, array $attributes)
44
    {
45
        $data = [];
46
47
        foreach ($attributes as $attribute => $value) {
48
            $data[call_user_func([$class, 'getColumnName'], $attribute)] = $value;
49
        }
50
51
        return $data;
52
    }
53
54
    /**
55
     * Create a partial mock of Entity $class
56
     *
57
     * *Note: the entity will get a random primary key if not predefined.*
58
     *
59
     * @param string        $class
60
     * @param array         $data
61
     * @return m\Mock|Entity
62
     */
63
    public function ormCreateMockedEntity($class, $data = [])
64
    {
65
        /** @var EntityManagerMock $em */
66
        $em = $this->ormGetEntityManagerInstance($class);
67
68
        /** @var Entity|m\MockInterface $entity */
69
        $entity = m::mock($class)->makePartial();
70
        $entity->setEntityManager($em);
71
        $entity->setOriginalData($this->ormAttributesToData($class, $data));
72
        $entity->reset();
73
74
        $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

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

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

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

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