Completed
Push — master ( 30ce3b...274fc3 )
by Jacob
12s
created

Persister::createSchemata()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 6

Duplication

Lines 9
Ratio 100 %

Importance

Changes 0
Metric Value
dl 9
loc 9
rs 9.6666
c 0
b 0
f 0
cc 3
eloc 6
nc 3
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
        $cursor = $this->getQuery()->executeFind($metadata, $store, $criteria, $fields, $sort, $offset, $limit);
234
        return $this->getHydrator()->createCursorRecordSet($metadata, $cursor, $store);
235
    }
236
237
    /**
238
     * {@inheritDoc}
239
     */
240
    public function retrieve(EntityMetadata $metadata, $identifier, Store $store)
241
    {
242
        $criteria = $this->getQuery()->getRetrieveCritiera($metadata, $identifier);
243
        $cursor = $this->getQuery()->executeFind($metadata, $store, $criteria);
244
        return $this->getHydrator()->createCursorRecordSet($metadata, $cursor, $store);
245
    }
246
247
    /**
248
     * {@inheritDoc}
249
     * @todo    Optimize the changeset to query generation.
250
     */
251
    public function update(Model $model)
252
    {
253
        $metadata = $model->getMetadata();
254
        $criteria = $this->getQuery()->getRetrieveCritiera($metadata, $model->getId());
255
        $update = $this->createUpdateObj($model);
256
257
        if (empty($update)) {
258
            return $model;
259
        }
260
261
        $this->getQuery()->executeUpdate($metadata, $model->getStore(), $criteria, $update);
262
        return $model;
263
    }
264
265
    /**
266
     * Appends the change set values to a database object based on the provided handler.
267
     *
268
     * @param   Model   $model
269
     * @param   array   $obj
270
     * @param   Closure $handler
271
     * @return  array
272
     */
273
    private function appendChangeSet(Model $model, array $obj, Closure $handler)
274
    {
275
        $metadata = $model->getMetadata();
276
        $changeset = $model->getChangeSet();
277
        $formatter = $this->getFormatter();
278
279
        foreach ($this->changeSetMethods as $setKey => $methods) {
280
            list($metaMethod, $formatMethod) = $methods;
281
            foreach ($changeset[$setKey] as $key => $values) {
282
                $value = $formatter->$formatMethod($metadata->$metaMethod($key), $values['new']);
283
                $obj = $handler($key, $value, $obj);
284
            }
285
        }
286
        return $obj;
287
    }
288
289
    /**
290
     * Creates the database insert object for a Model.
291
     *
292
     * @param   Model   $model
293
     * @return  array
294
     */
295
    private function createInsertObj(Model $model)
296
    {
297
        $metadata = $model->getMetadata();
298
        $insert = [
299
            $this->getIdentifierKey()   => $this->convertId($model->getId()),
300
        ];
301
        if (true === $metadata->isChildEntity()) {
302
            $insert[$this->getPolymorphicKey()] = $metadata->type;
303
        }
304
        return $this->appendChangeSet($model, $insert, $this->getCreateChangeSetHandler());
305
    }
306
307
    /**
308
     * Creates the database update object for a Model.
309
     *
310
     * @param   Model   $model
311
     * @return  array
312
     */
313
    private function createUpdateObj(Model $model)
314
    {
315
        return $this->appendChangeSet($model, [], $this->getUpdateChangeSetHandler());
316
    }
317
318
    /**
319
     * Gets the change set handler Closure for create.
320
     *
321
     * @return  Closure
322
     */
323
    private function getCreateChangeSetHandler()
324
    {
325
        return function ($key, $value, $obj) {
326
            if (null !== $value) {
327
                $obj[$key] = $value;
328
            }
329
            return $obj;
330
        };
331
    }
332
333
    /**
334
     * Gets the change set handler Closure for update.
335
     *
336
     * @return  Closure
337
     */
338
    private function getUpdateChangeSetHandler()
339
    {
340
        return function ($key, $value, $obj) {
341
            $op = '$set';
342
            if (null === $value) {
343
                $op = '$unset';
344
                $value = 1;
345
            }
346
            $obj[$op][$key] = $value;
347
            return $obj;
348
        };
349
    }
350
}
351