Completed
Push — master ( 0ca7eb...4410d5 )
by Karsten
03:24
created

MongoDbStorageDriver   C

Complexity

Total Complexity 32

Size/Duplication

Total Lines 362
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 21

Test Coverage

Coverage 88.52%

Importance

Changes 0
Metric Value
wmc 32
lcom 1
cbo 21
dl 0
loc 362
ccs 108
cts 122
cp 0.8852
rs 5.8666
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A getEntityBaseClass() 0 4 1
A buildIndexes() 0 4 1
B findOneInternal() 0 26 6
A insertInternal() 0 22 2
A saveInternal() 0 55 4
A removeInternal() 0 10 1
A removeAll() 0 8 1
A removeAllInternal() 0 10 2
A getItemId() 0 4 1
A setItemId() 0 4 1
A invokePostSaveListeners() 0 12 3
A guard() 0 4 1
A find() 0 8 1
B findInternal() 0 24 2
A findOne() 0 8 1
A insert() 0 8 1
A save() 0 8 1
A remove() 0 8 1
1
<?php
2
/**
3
 * Created by IntelliJ IDEA.
4
 * User: gerk
5
 * Date: 06.01.17
6
 * Time: 11:57
7
 */
8
9
namespace PeekAndPoke\Component\Slumber\Data\MongoDb;
10
11
use MongoDB\Collection;
12
use MongoDB\Driver\Exception\WriteException;
13
use PeekAndPoke\Component\Slumber\Data\AwakingCursorIterator;
14
use PeekAndPoke\Component\Slumber\Data\Cursor;
15
use PeekAndPoke\Component\Slumber\Data\EntityPool;
16
use PeekAndPoke\Component\Slumber\Data\Error\DuplicateError;
17
use PeekAndPoke\Component\Slumber\Data\MongoDb\Error\MongoDbDuplicateError;
18
use PeekAndPoke\Component\Slumber\Data\Result;
19
use PeekAndPoke\Component\Slumber\Data\StorageDriver;
20
21
/**
22
 * @api
23
 *
24
 * @author Karsten J. Gerber <[email protected]>
25
 *
26
 * @see    https://docs.mongodb.com/php-library/master/tutorial/
27
 */
28
class MongoDbStorageDriver implements StorageDriver
29
{
30
    /** @var EntityPool */
31
    private $entityPool;
32
    /** @var Collection */
33
    private $collection;
34
    /** @var MongoDbCodecSet */
35
    private $codecSet;
36
    /** @var \ReflectionClass */
37
    private $entityBaseClass;
38
    /** @var MongoDbEntityConfig */
39
    private $entityConfig;
40
41
    public function __construct(EntityPool $entityPool, MongoDbCodecSet $codecSet, Collection $collection, \ReflectionClass $entityBaseClass)
42
    {
43
        $this->entityPool      = $entityPool;
44
        $this->codecSet        = $codecSet;
45
        $this->collection      = $collection;
46
        $this->entityBaseClass = $entityBaseClass;
47
        $this->entityConfig    = $codecSet->getLookUp()->getEntityConfig($entityBaseClass);
48
    }
49
50
    /**
51
     * @return \ReflectionClass
52
     */
53 27
    public function getEntityBaseClass()
54
    {
55 27
        return $this->entityBaseClass;
56
    }
57
58
    /**
59
     * @return array
60
     */
61
    public function buildIndexes()
62
    {
63
        return $this->codecSet->getIndexer()->ensureIndexes($this->collection, $this->entityBaseClass);
64
    }
65
66
    /**
67
     * @param $query
68
     *
69
     * @return Cursor
70
     */
71 11
    public function find(array $query = null)
72
    {
73 11
        return $this->guard(
74 11
            function () use ($query) {
75 11
                return $this->findInternal($query);
76 11
            }
77
        );
78
    }
79
80
    /**
81
     * @param $query
82
     *
83
     * @return Cursor
84
     */
85 11
    private function findInternal(array $query = null)
86
    {
87 11
        $query = $query ?: [];
88
89
        // We do not provide the cursor right away.
90
        // By doing so we post pone the query until the data is really requested by iterating it.
91 11
        $cursorProvider = function ($options) use ($query) {
92
93
            // we want raw php arrays as return types
94 6
            $options['typeMap'] = ['root' => 'array', 'document' => 'array', 'array' => 'array'];
95
96 6
            return new AwakingCursorIterator(
97 6
                $this->collection->find($query, $options),
98 4
                $this->codecSet->getAwaker(),
99 4
                $this->entityBaseClass
100
            );
101 11
        };
102
103 11
        $countProvider = function ($options) use ($query) {
104 7
            return $this->collection->count($query, $options);
105 11
        };
106
107 11
        return new MongoDbCursor($cursorProvider, $countProvider);
108
    }
109
110
    /**
111
     * @param array|null $query
112
     *
113
     * @return null
114
     */
115 19
    public function findOne(array $query = null)
116
    {
117 19
        return $this->guard(
118 19
            function () use ($query) {
119 19
                return $this->findOneInternal($query);
120 19
            }
121
        );
122
    }
123
124
    /**
125
     * @param array|null $query
126
     *
127
     * @return null
128
     */
129 19
    private function findOneInternal(array $query = null)
130
    {
131
        // TODO: find a more encapsulated way for looking it up in the pool
132
        //       can we use the propertyAccess of the ID and use the propertyName? Is the alright ?
133
134
        // do we have it in the pool ?
135 19
        if (count($query) === 1
136 19
            && isset($query['_id'])
137 19
            && $this->entityPool->has($this->entityBaseClass, EntityPool::PRIMARY_ID, (string) $query['_id'])
138
        ) {
139 3
            return $this->entityPool->get($this->entityBaseClass, EntityPool::PRIMARY_ID, (string) $query['_id']);
140
        }
141
142 18
        $result = $this->collection->findOne(
143 18
            $query ?: [],
144
            [
145 18
                'typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array'] // we want raw php arrays as return types
146
            ]
147
        );
148
149 18
        if ($result === null) {
150
            return null;
151
        }
152
153 18
        return $this->codecSet->getAwaker()->awake($result, $this->entityBaseClass);
154
    }
155
156
    /**
157
     * @param mixed $item
158
     *
159
     * @return Result\InsertOneResult
160
     *
161
     * @throws DuplicateError
162
     */
163 3
    public function insert($item)
164
    {
165 3
        return $this->guard(
166 3
            function () use ($item) {
167 3
                return $this->insertInternal($item);
168 3
            }
169
        );
170
    }
171
172
    /**
173
     * @param mixed $item
174
     *
175
     * @return Result\InsertOneResult
176
     *
177
     * @throws DuplicateError
178
     */
179 3
    private function insertInternal($item)
180
    {
181 3
        $slumbering = $this->codecSet->getSlumberer()->slumber($item);
182
183 3
        if (empty($slumbering['_id'])) {
184 2
            unset($slumbering['_id']);
185
        }
186
187 3
        $result = $this->collection->insertOne($slumbering);
0 ignored issues
show
Bug introduced by
It seems like $slumbering defined by $this->codecSet->getSlumberer()->slumber($item) on line 181 can also be of type string; however, MongoDB\Collection::insertOne() does only seem to accept array|object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
188
189
        // write back the id
190 2
        $insertedId = (string) $result->getInsertedId();
191 2
        $this->setItemId($item, $insertedId);
192
193
        // set it on the entity pool
194 2
        $this->entityPool->set($this->entityBaseClass, EntityPool::PRIMARY_ID, $insertedId, $item);
195
196
        // dispatch post save events (e.g. for the Journal)
197 2
        $this->invokePostSaveListeners($item, $slumbering);
0 ignored issues
show
Bug introduced by
It seems like $slumbering defined by $this->codecSet->getSlumberer()->slumber($item) on line 181 can also be of type string; however, PeekAndPoke\Component\Sl...vokePostSaveListeners() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
198
199 2
        return new Result\InsertOneResult($insertedId, $result->isAcknowledged());
200
    }
201
202
    /**
203
     * Insert an item
204
     *
205
     * @param mixed $item
206
     *
207
     * @return Result\SaveOneResult
208
     *
209
     * @throws DuplicateError
210
     */
211 24
    public function save($item)
212
    {
213 24
        return $this->guard(
214 24
            function () use ($item) {
215 24
                return $this->saveInternal($item);
216 24
            }
217
        );
218
    }
219
220
    /**
221
     * Insert an item
222
     *
223
     * @param mixed $item
224
     *
225
     * @return Result\SaveOneResult
226
     *
227
     * @throws DuplicateError
228
     */
229 24
    private function saveInternal($item)
230
    {
231 24
        $slumbering = $this->codecSet->getSlumberer()->slumber($item);
232
233
        ////  CREATE OR UPDATE ?  //////////////////////////////////////////////////////////////////////////////////////
234
235 24
        if (empty($slumbering['_id'])) {
236
237
            // unset the _id so we get an id created
238 22
            unset ($slumbering['_id']);
239
240
            try {
241 22
                $insertOneResult = $this->collection->insertOne($slumbering);
0 ignored issues
show
Bug introduced by
It seems like $slumbering defined by $this->codecSet->getSlumberer()->slumber($item) on line 231 can also be of type string; however, MongoDB\Collection::insertOne() does only seem to accept array|object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
242 1
            } catch (WriteException $e) {
243
                throw MongoDbDuplicateError::from($e);
244
            }
245
246
            // write back the id
247 21
            $insertedId = (string) $insertOneResult->getInsertedId();
248 21
            $this->setItemId($item, $insertedId);
249
250
            // set it on the entity pool
251 21
            $this->entityPool->set($this->entityBaseClass, EntityPool::PRIMARY_ID, $insertedId, $item);
252
253
            // build return result
254 21
            $result = new Result\SaveOneResult($insertedId, $insertOneResult->isAcknowledged(), false);
255
256
        } else {
257
            // UPDATE or INSERT with specific id
258
            try {
259 8
                $updateOneResult = $this->collection->updateOne(
260 8
                    ['_id' => MongoDbUtil::ensureMongoId($slumbering['_id'])],
261 8
                    ['$set' => $slumbering],
262 8
                    ['upsert' => true]
263
                );
264
            } catch (WriteException $e) {
265
                throw MongoDbDuplicateError::from($e);
266
            }
267
268
            // save it on the entity pool
269 8
            $this->entityPool->set($this->entityBaseClass, EntityPool::PRIMARY_ID, $slumbering['_id'], $item);
270
271
            // build return result
272 8
            $result = new Result\SaveOneResult(
273 8
                $updateOneResult->getUpsertedId(),
274 8
                $updateOneResult->isAcknowledged(),
275 8
                $updateOneResult->getUpsertedCount() === 1
276
            );
277
        }
278
279
        // dispatch post save events (e.g. for the Journal)
280 23
        $this->invokePostSaveListeners($item, $slumbering);
0 ignored issues
show
Bug introduced by
It seems like $slumbering defined by $this->codecSet->getSlumberer()->slumber($item) on line 231 can also be of type string; however, PeekAndPoke\Component\Sl...vokePostSaveListeners() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
281
282 23
        return $result;
283
    }
284
285
    /**
286
     * @param mixed $entity
287
     *
288
     * @return Result\RemoveResult
289
     */
290 3
    public function remove($entity)
291
    {
292 3
        return $this->guard(
293 3
            function () use ($entity) {
294 3
                return $this->removeInternal($entity);
295 3
            }
296
        );
297
    }
298
299
    /**
300
     * @param mixed $entity
301
     *
302
     * @return Result\RemoveResult
303
     */
304 3
    private function removeInternal($entity)
305
    {
306 3
        $id = $this->getItemId($entity);
307
308 3
        $result = $this->collection->deleteOne([
309 3
            '_id' => MongoDbUtil::ensureMongoId($id),
310
        ]);
311
312 2
        return new Result\RemoveResult($result->getDeletedCount(), $result->isAcknowledged());
313
    }
314
315
    /**
316
     * Remove all from this collection
317
     *
318
     * @param array|null $query
319
     *
320
     * @return Result\RemoveResult
321
     */
322 24
    public function removeAll(array $query = null)
323
    {
324 24
        return $this->guard(
325 24
            function () use ($query) {
326 24
                return $this->removeAllInternal($query);
327 24
            }
328
        );
329
    }
330
331
    /**
332
     * Remove all from this collection
333
     *
334
     * @param array|null $query
335
     *
336
     * @return Result\RemoveResult
337
     */
338 24
    public function removeAllInternal(array $query = null)
339
    {
340 24
        if ($query === null) {
341
            return new Result\RemoveResult(0, false);
342
        }
343
344 24
        $result = $this->collection->deleteMany($query);
345
346 23
        return new Result\RemoveResult($result->getDeletedCount(), $result->isAcknowledged());
347
    }
348
349
    /**
350
     * @param mixed $item
351
     *
352
     * @return mixed
353
     */
354 3
    private function getItemId($item)
355
    {
356 3
        return $this->entityConfig->getIdAccess()->get($item);
357
    }
358
359
    /**
360
     * @param mixed $entity
361
     * @param mixed $id
362
     */
363 23
    private function setItemId($entity, $id)
364
    {
365 23
        $this->entityConfig->getIdAccess()->set($entity, $id);
366 23
    }
367
368
    /**
369
     * @param mixed $item       The item that was saved
370
     * @param array $slumbering The serialized data
371
     */
372 25
    private function invokePostSaveListeners($item, $slumbering)
373
    {
374
        // dispatch post save events (e.g. for the Journal)
375 25
        if ($this->entityConfig->hasPostSaveClassListeners()) {
376
377 4
            $postSaveEvent = $this->codecSet->createPostSaveEventFor($item, $slumbering);
378
379 4
            foreach ($this->entityConfig->getPostSaveClassListeners() as $postSave) {
380 4
                $postSave->execute($postSaveEvent);
381
            }
382
        }
383 25
    }
384
385 33
    private function guard(callable $action)
386
    {
387 33
        return MongoDbGuard::guard($action);
388
    }
389
}
390