Persister::generateId()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 1
1
<?php
2
3
namespace As3\Modlr\Persister\MongoDb;
4
5
use \Closure;
6
use \MongoId;
7
use As3\Modlr\Metadata\EntityMetadata;
8
use As3\Modlr\Models\Model;
9
use As3\Modlr\Persister\PersisterException;
10
use As3\Modlr\Persister\PersisterInterface;
11
use As3\Modlr\Store\Store;
12
13
/**
14
 * Persists and retrieves models to/from a MongoDB database.
15
 *
16
 * @author Jacob Bare <[email protected]>
17
 */
18
final class Persister implements PersisterInterface
19
{
20
    const IDENTIFIER_KEY    = '_id';
21
    const POLYMORPHIC_KEY   = '_type';
22
    const PERSISTER_KEY     = 'mongodb';
23
24
    /**
25
     * Provides a map of changeset methods.
26
     *
27
     * @var array
28
     */
29
    private $changeSetMethods = [
30
        'attributes'    => ['getAttribute', 'getAttributeDbValue'],
31
        'hasOne'        => ['getRelationship', 'getHasOneDbValue'],
32
        'hasMany'       => ['getRelationship', 'getHasManyDbValue'],
33
        'embedOne'      => ['getEmbed', 'getEmbedOneDbValue'],
34
        'embedMany'     => ['getEmbed', 'getEmbedManyDbValue'],
35
    ];
36
37
    /**
38
     * The raw result hydrator.
39
     *
40
     * @var Hydrator
41
     */
42
    private $hydrator;
43
44
    /**
45
     * @var SchemaManager
46
     */
47
    private $schemaManager;
48
49
    /**
50
     * @var StorageMetadataFactory
51
     */
52
    private $smf;
53
54
    /**
55
     * The query service.
56
     *
57
     * @var Query
58
     */
59
    private $query;
60
61
    /**
62
     * Constructor.
63
     *
64
     * @param   Query                   $query
65
     * @param   StorageMetadataFactory  $smf
66
     * @param   Hydrator                $hydrator
67
     */
68
    public function __construct(Query $query, StorageMetadataFactory $smf, Hydrator $hydrator, SchemaManager $schemaManager)
69
    {
70
        $this->smf = $smf;
71
        $this->query = $query;
72
        $this->hydrator = $hydrator;
73
        $this->schemaManager = $schemaManager;
74
    }
75
76
    /**
77
     * {@inheritDoc}
78
     */
79
    public function all(EntityMetadata $metadata, Store $store, array $identifiers = [], array $fields = [], array $sort = [], $offset = 0, $limit = 0)
80
    {
81
        $criteria = $this->getQuery()->getRetrieveCritiera($metadata, $identifiers);
82
        $cursor = $this->getQuery()->executeFind($metadata, $store, $criteria, $fields, $sort, $offset, $limit);
83
        return $this->getHydrator()->createCursorRecordSet($metadata, $cursor, $store);
84
    }
85
86
    /**
87
     * {@inheritDoc}
88
     */
89
    public function convertId($identifier, $strategy = null)
90
    {
91
        return $this->getFormatter()->getIdentifierDbValue($identifier, $strategy);
92
    }
93
94
    /**
95
     * {@inheritDoc}
96
     * @todo    Optimize the changeset to query generation.
97
     */
98
    public function create(Model $model)
99
    {
100
        $metadata = $model->getMetadata();
101
        $insert = $this->createInsertObj($model);
102
        $this->getQuery()->executeInsert($metadata, $insert);
103
        return $model;
104
    }
105
106
    /**
107
     * {@inheritDoc}
108
     */
109 View Code Duplication
    public function createSchemata(EntityMetadata $metadata)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
110
    {
111
        if (!$metadata->persistence instanceof StorageMetadata) {
112
            throw PersisterException::badRequest('Wrong StorageMetadata type');
113
        }
114
        $collection = $this->getQuery()->getModelCollection($metadata);
115
        $schemata = $metadata->persistence->schemata ?: [];
116
        return $this->schemaManager->createSchemata($collection, $schemata);
117
    }
118
119
    /**
120
     * {@inheritDoc}
121
     */
122 View Code Duplication
    public function syncSchemata(EntityMetadata $metadata)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
123
    {
124
        if (!$metadata->persistence instanceof StorageMetadata) {
125
            throw PersisterException::badRequest('Wrong StorageMetadata type');
126
        }
127
        $collection = $this->getQuery()->getModelCollection($metadata);
128
        $schemata = $metadata->persistence->schemata ?: [];
0 ignored issues
show
Unused Code introduced by
$schemata is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
129
        return $this->schemaManager->syncSchemata($collection);
0 ignored issues
show
Bug introduced by
The call to syncSchemata() misses a required argument $schemata.

This check looks for function calls that miss required arguments.

Loading history...
130
    }
131
132
    /**
133
     * {@inheritDoc}
134
     */
135
    public function delete(Model $model)
136
    {
137
        $metadata = $model->getMetadata();
138
        $criteria = $this->getQuery()->getRetrieveCritiera($metadata, $model->getId());
139
        $this->getQuery()->executeDelete($metadata, $model->getStore(), $criteria);
140
        return $model;
141
    }
142
143
    /**
144
     * {@inheritDoc}
145
     */
146
    public function extractType(EntityMetadata $metadata, array $data)
147
    {
148
        return $this->getHydrator()->extractType($metadata, $data);
149
    }
150
151
    /**
152
     * {@inheritDoc}
153
     */
154
    public function generateId($strategy = null)
155
    {
156
        if (false === $this->getFormatter()->isIdStrategySupported($strategy)) {
157
            throw PersisterException::nyi('ID generation currently only supports an object strategy, or none at all.');
158
        }
159
        return new MongoId();
160
    }
161
162
    /**
163
     * @return  Formatter
164
     */
165
    public function getFormatter()
166
    {
167
        return $this->getQuery()->getFormatter();
168
    }
169
170
    /**
171
     * @return  Hydrator
172
     */
173
    public function getHydrator()
174
    {
175
        return $this->hydrator;
176
    }
177
178
    /**
179
     * {@inheritDoc}
180
     */
181
    public function getIdentifierKey()
182
    {
183
        return self::IDENTIFIER_KEY;
184
    }
185
186
    /**
187
     * {@inheritDoc}
188
     */
189
    public function getPersistenceMetadataFactory()
190
    {
191
        return $this->smf;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->smf; (As3\Modlr\Persister\MongoDb\StorageMetadataFactory) is incompatible with the return type declared by the interface As3\Modlr\Persister\Pers...sistenceMetadataFactory of type As3\Modlr\Metadata\Inter...etadataFactoryInterface.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
192
    }
193
194
    /**
195
     * {@inheritDoc}
196
     */
197
    public function getPersisterKey()
198
    {
199
        return self::PERSISTER_KEY;
200
    }
201
202
    /**
203
     * {@inheritDoc}
204
     */
205
    public function getPolymorphicKey()
206
    {
207
        return self::POLYMORPHIC_KEY;
208
    }
209
210
    /**
211
     * @return Query
212
     */
213
    public function getQuery()
214
    {
215
        return $this->query;
216
    }
217
218
    /**
219
     * {@inheritDoc}
220
     */
221
    public function inverse(EntityMetadata $owner, EntityMetadata $rel, Store $store, array $identifiers, $inverseField)
222
    {
223
        $criteria = $this->getQuery()->getInverseCriteria($owner, $rel, $identifiers, $inverseField);
224
        $cursor = $this->getQuery()->executeFind($rel, $store, $criteria);
225
        return $this->getHydrator()->createCursorRecordSet($rel, $cursor, $store);
226
    }
227
228
    /**
229
     * {@inheritDoc}
230
     */
231
    public function query(EntityMetadata $metadata, Store $store, array $criteria, array $fields = [], array $sort = [], $offset = 0, $limit = 0)
232
    {
233
        $criteria = array_merge($criteria, $this->getQuery()->getRetrieveCritiera($metadata));
234
        $cursor = $this->getQuery()->executeFind($metadata, $store, $criteria, $fields, $sort, $offset, $limit);
235
        return $this->getHydrator()->createCursorRecordSet($metadata, $cursor, $store);
236
    }
237
238
    /**
239
     * {@inheritDoc}
240
     */
241
    public function retrieve(EntityMetadata $metadata, $identifier, Store $store)
242
    {
243
        $criteria = $this->getQuery()->getRetrieveCritiera($metadata, $identifier);
244
        $cursor = $this->getQuery()->executeFind($metadata, $store, $criteria);
245
        return $this->getHydrator()->createCursorRecordSet($metadata, $cursor, $store);
246
    }
247
248
    /**
249
     * {@inheritDoc}
250
     * @todo    Optimize the changeset to query generation.
251
     */
252
    public function update(Model $model)
253
    {
254
        $metadata = $model->getMetadata();
255
        $criteria = $this->getQuery()->getRetrieveCritiera($metadata, $model->getId());
256
        $update = $this->createUpdateObj($model);
257
258
        if (empty($update)) {
259
            return $model;
260
        }
261
262
        $this->getQuery()->executeUpdate($metadata, $model->getStore(), $criteria, $update);
263
        return $model;
264
    }
265
266
    /**
267
     * Appends the change set values to a database object based on the provided handler.
268
     *
269
     * @param   Model   $model
270
     * @param   array   $obj
271
     * @param   Closure $handler
272
     * @return  array
273
     */
274
    private function appendChangeSet(Model $model, array $obj, Closure $handler)
275
    {
276
        $metadata = $model->getMetadata();
277
        $changeset = $model->getChangeSet();
278
        $formatter = $this->getFormatter();
279
280
        foreach ($this->changeSetMethods as $setKey => $methods) {
281
            list($metaMethod, $formatMethod) = $methods;
282
            foreach ($changeset[$setKey] as $key => $values) {
283
                $value = $formatter->$formatMethod($metadata->$metaMethod($key), $values['new']);
284
                $obj = $handler($key, $value, $obj);
285
            }
286
        }
287
        return $obj;
288
    }
289
290
    /**
291
     * Creates the database insert object for a Model.
292
     *
293
     * @param   Model   $model
294
     * @return  array
295
     */
296
    private function createInsertObj(Model $model)
297
    {
298
        $metadata = $model->getMetadata();
299
        $insert = [
300
            $this->getIdentifierKey()   => $this->convertId($model->getId()),
301
        ];
302
        if (true === $metadata->isChildEntity()) {
303
            $insert[$this->getPolymorphicKey()] = $metadata->type;
304
        }
305
        return $this->appendChangeSet($model, $insert, $this->getCreateChangeSetHandler());
306
    }
307
308
    /**
309
     * Creates the database update object for a Model.
310
     *
311
     * @param   Model   $model
312
     * @return  array
313
     */
314
    private function createUpdateObj(Model $model)
315
    {
316
        return $this->appendChangeSet($model, [], $this->getUpdateChangeSetHandler());
317
    }
318
319
    /**
320
     * Gets the change set handler Closure for create.
321
     *
322
     * @return  Closure
323
     */
324
    private function getCreateChangeSetHandler()
325
    {
326
        return function ($key, $value, $obj) {
327
            if (null !== $value) {
328
                $obj[$key] = $value;
329
            }
330
            return $obj;
331
        };
332
    }
333
334
    /**
335
     * Gets the change set handler Closure for update.
336
     *
337
     * @return  Closure
338
     */
339
    private function getUpdateChangeSetHandler()
340
    {
341
        return function ($key, $value, $obj) {
342
            $op = '$set';
343
            if (null === $value) {
344
                $op = '$unset';
345
                $value = 1;
346
            }
347
            $obj[$op][$key] = $value;
348
            return $obj;
349
        };
350
    }
351
}
352