Completed
Pull Request — master (#16)
by Joshua
02:23
created

Persister   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 327
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
wmc 34
lcom 1
cbo 7
dl 0
loc 327
rs 9.2
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A all() 0 6 1
A convertId() 0 4 1
A create() 0 7 1
A createSchemata() 0 6 2
A syncSchemata() 0 6 2
A delete() 0 7 1
A extractType() 0 4 1
A generateId() 0 7 2
A getFormatter() 0 4 1
A getHydrator() 0 4 1
A getIdentifierKey() 0 4 1
A getPersistenceMetadataFactory() 0 4 1
A getPersisterKey() 0 4 1
A getPolymorphicKey() 0 4 1
A getQuery() 0 4 1
A inverse() 0 6 1
A query() 0 5 1
A retrieve() 0 6 1
A update() 0 13 2
A appendChangeSet() 0 15 3
A createInsertObj() 0 11 2
A createUpdateObj() 0 4 1
A getCreateChangeSetHandler() 0 9 2
A getUpdateChangeSetHandler() 0 12 2
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
    public function createSchemata(EntityMetadata $metadata)
110
    {
111
        $collection = $this->getQuery()->getModelCollection($metadata);
112
        $schemata = $metadata->persistence->schemata ?: [];
0 ignored issues
show
Bug introduced by
Accessing schemata on the interface As3\Modlr\Metadata\Inter...s\StorageLayerInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
113
        return $this->schemaManager->createSchemata($collection, $schemata);
114
    }
115
116
    /**
117
     * {@inheritDoc}
118
     */
119
    public function syncSchemata(EntityMetadata $metadata)
120
    {
121
        $collection = $this->getQuery()->getModelCollection($metadata);
122
        $schemata = $metadata->persistence->schemata ?: [];
0 ignored issues
show
Bug introduced by
Accessing schemata on the interface As3\Modlr\Metadata\Inter...s\StorageLayerInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
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...
123
        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...
124
    }
125
126
    /**
127
     * {@inheritDoc}
128
     */
129
    public function delete(Model $model)
130
    {
131
        $metadata = $model->getMetadata();
132
        $criteria = $this->getQuery()->getRetrieveCritiera($metadata, $model->getId());
133
        $this->getQuery()->executeDelete($metadata, $model->getStore(), $criteria);
134
        return $model;
135
    }
136
137
    /**
138
     * {@inheritDoc}
139
     */
140
    public function extractType(EntityMetadata $metadata, array $data)
141
    {
142
        return $this->getHydrator()->extractType($metadata, $data);
143
    }
144
145
    /**
146
     * {@inheritDoc}
147
     */
148
    public function generateId($strategy = null)
149
    {
150
        if (false === $this->getFormatter()->isIdStrategySupported($strategy)) {
151
            throw PersisterException::nyi('ID generation currently only supports an object strategy, or none at all.');
152
        }
153
        return new MongoId();
154
    }
155
156
    /**
157
     * @return  Formatter
158
     */
159
    public function getFormatter()
160
    {
161
        return $this->getQuery()->getFormatter();
162
    }
163
164
    /**
165
     * @return  Hydrator
166
     */
167
    public function getHydrator()
168
    {
169
        return $this->hydrator;
170
    }
171
172
    /**
173
     * {@inheritDoc}
174
     */
175
    public function getIdentifierKey()
176
    {
177
        return self::IDENTIFIER_KEY;
178
    }
179
180
    /**
181
     * {@inheritDoc}
182
     */
183
    public function getPersistenceMetadataFactory()
184
    {
185
        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...
186
    }
187
188
    /**
189
     * {@inheritDoc}
190
     */
191
    public function getPersisterKey()
192
    {
193
        return self::PERSISTER_KEY;
194
    }
195
196
    /**
197
     * {@inheritDoc}
198
     */
199
    public function getPolymorphicKey()
200
    {
201
        return self::POLYMORPHIC_KEY;
202
    }
203
204
    /**
205
     * @return Query
206
     */
207
    public function getQuery()
208
    {
209
        return $this->query;
210
    }
211
212
    /**
213
     * {@inheritDoc}
214
     */
215
    public function inverse(EntityMetadata $owner, EntityMetadata $rel, Store $store, array $identifiers, $inverseField)
216
    {
217
        $criteria = $this->getQuery()->getInverseCriteria($owner, $rel, $identifiers, $inverseField);
218
        $cursor = $this->getQuery()->executeFind($rel, $store, $criteria);
219
        return $this->getHydrator()->createCursorRecordSet($rel, $cursor, $store);
220
    }
221
222
    /**
223
     * {@inheritDoc}
224
     */
225
    public function query(EntityMetadata $metadata, Store $store, array $criteria, array $fields = [], array $sort = [], $offset = 0, $limit = 0)
226
    {
227
        $cursor = $this->getQuery()->executeFind($metadata, $store, $criteria, $fields, $sort, $offset, $limit);
228
        return $this->getHydrator()->createCursorRecordSet($metadata, $cursor, $store);
229
    }
230
231
    /**
232
     * {@inheritDoc}
233
     */
234
    public function retrieve(EntityMetadata $metadata, $identifier, Store $store)
235
    {
236
        $criteria = $this->getQuery()->getRetrieveCritiera($metadata, $identifier);
237
        $cursor = $this->getQuery()->executeFind($metadata, $store, $criteria);
238
        return $this->getHydrator()->createCursorRecordSet($metadata, $cursor, $store);
239
    }
240
241
    /**
242
     * {@inheritDoc}
243
     * @todo    Optimize the changeset to query generation.
244
     */
245
    public function update(Model $model)
246
    {
247
        $metadata = $model->getMetadata();
248
        $criteria = $this->getQuery()->getRetrieveCritiera($metadata, $model->getId());
249
        $update = $this->createUpdateObj($model);
250
251
        if (empty($update)) {
252
            return $model;
253
        }
254
255
        $this->getQuery()->executeUpdate($metadata, $model->getStore(), $criteria, $update);
256
        return $model;
257
    }
258
259
    /**
260
     * Appends the change set values to a database object based on the provided handler.
261
     *
262
     * @param   Model   $model
263
     * @param   array   $obj
264
     * @param   Closure $handler
265
     * @return  array
266
     */
267
    private function appendChangeSet(Model $model, array $obj, Closure $handler)
268
    {
269
        $metadata = $model->getMetadata();
270
        $changeset = $model->getChangeSet();
271
        $formatter = $this->getFormatter();
272
273
        foreach ($this->changeSetMethods as $setKey => $methods) {
274
            list($metaMethod, $formatMethod) = $methods;
275
            foreach ($changeset[$setKey] as $key => $values) {
276
                $value = $formatter->$formatMethod($metadata->$metaMethod($key), $values['new']);
277
                $obj = $handler($key, $value, $obj);
278
            }
279
        }
280
        return $obj;
281
    }
282
283
    /**
284
     * Creates the database insert object for a Model.
285
     *
286
     * @param   Model   $model
287
     * @return  array
288
     */
289
    private function createInsertObj(Model $model)
290
    {
291
        $metadata = $model->getMetadata();
292
        $insert = [
293
            $this->getIdentifierKey()   => $this->convertId($model->getId()),
294
        ];
295
        if (true === $metadata->isChildEntity()) {
296
            $insert[$this->getPolymorphicKey()] = $metadata->type;
297
        }
298
        return $this->appendChangeSet($model, $insert, $this->getCreateChangeSetHandler());
299
    }
300
301
    /**
302
     * Creates the database update object for a Model.
303
     *
304
     * @param   Model   $model
305
     * @return  array
306
     */
307
    private function createUpdateObj(Model $model)
308
    {
309
        return $this->appendChangeSet($model, [], $this->getUpdateChangeSetHandler());
310
    }
311
312
    /**
313
     * Gets the change set handler Closure for create.
314
     *
315
     * @return  Closure
316
     */
317
    private function getCreateChangeSetHandler()
318
    {
319
        return function ($key, $value, $obj) {
320
            if (null !== $value) {
321
                $obj[$key] = $value;
322
            }
323
            return $obj;
324
        };
325
    }
326
327
    /**
328
     * Gets the change set handler Closure for update.
329
     *
330
     * @return  Closure
331
     */
332
    private function getUpdateChangeSetHandler()
333
    {
334
        return function ($key, $value, $obj) {
335
            $op = '$set';
336
            if (null === $value) {
337
                $op = '$unset';
338
                $value = 1;
339
            }
340
            $obj[$op][$key] = $value;
341
            return $obj;
342
        };
343
    }
344
}
345