Completed
Push — master ( 16ac11...a34a29 )
by Jacob
7s
created

Persister::create()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 1 Features 2
Metric Value
c 5
b 1
f 2
dl 0
loc 7
rs 9.4285
cc 1
eloc 5
nc 1
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
    ];
34
35
    /**
36
     * The raw result hydrator.
37
     *
38
     * @var Hydrator
39
     */
40
    private $hydrator;
41
42
    /**
43
     * @var StorageMetadataFactory
44
     */
45
    private $smf;
46
47
    /**
48
     * The query service.
49
     *
50
     * @var Query
51
     */
52
    private $query;
53
54
    /**
55
     * Constructor.
56
     *
57
     * @param   Query                   $query
58
     * @param   StorageMetadataFactory  $smf
59
     */
60
    public function __construct(Query $query, StorageMetadataFactory $smf)
61
    {
62
        $this->hydrator = new Hydrator();
63
        $this->smf = $smf;
64
        $this->query = $query;
65
    }
66
67
    /**
68
     * {@inheritDoc}
69
     */
70
    public function all(EntityMetadata $metadata, Store $store, array $identifiers = [])
71
    {
72
        $criteria = $this->getQuery()->getRetrieveCritiera($metadata, $identifiers);
73
        $cursor = $this->getQuery()->executeFind($metadata, $store, $criteria);
74
        return $this->getHydrator()->hydrateMany($metadata, $cursor->toArray(), $store);
75
    }
76
77
    /**
78
     * {@inheritDoc}
79
     */
80
    public function convertId($identifier, $strategy = null)
81
    {
82
        return $this->getFormatter()->getIdentifierDbValue($identifier, $strategy);
83
    }
84
85
    /**
86
     * {@inheritDoc}
87
     * @todo    Optimize the changeset to query generation.
88
     */
89
    public function create(Model $model)
90
    {
91
        $metadata = $model->getMetadata();
92
        $insert = $this->createInsertObj($model);
93
        $this->getQuery()->executeInsert($metadata, $insert);
94
        return $model;
95
    }
96
97
    /**
98
     * {@inheritDoc}
99
     */
100
    public function delete(Model $model)
101
    {
102
        $metadata = $model->getMetadata();
103
        $criteria = $this->getQuery()->getRetrieveCritiera($metadata, $model->getId());
104
        $this->getQuery()->executeDelete($metadata, $model->getStore(), $criteria);
105
        return $model;
106
    }
107
108
    /**
109
     * {@inheritDoc}
110
     */
111
    public function extractType(EntityMetadata $metadata, array $data)
112
    {
113
        return $this->getHydrator()->extractType($metadata, $data);
114
    }
115
116
    /**
117
     * {@inheritDoc}
118
     */
119
    public function generateId($strategy = null)
120
    {
121
        if (false === $this->getFormatter()->isIdStrategySupported($strategy)) {
122
            throw PersisterException::nyi('ID generation currently only supports an object strategy, or none at all.');
123
        }
124
        return new MongoId();
125
    }
126
127
    /**
128
     * @return  Formatter
129
     */
130
    public function getFormatter()
131
    {
132
        return $this->getQuery()->getFormatter();
133
    }
134
135
    /**
136
     * @return  Hydrator
137
     */
138
    public function getHydrator()
139
    {
140
        return $this->hydrator;
141
    }
142
143
    /**
144
     * {@inheritDoc}
145
     */
146
    public function getIdentifierKey()
147
    {
148
        return self::IDENTIFIER_KEY;
149
    }
150
151
    /**
152
     * {@inheritDoc}
153
     */
154
    public function getPersistenceMetadataFactory()
155
    {
156
        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...
157
    }
158
159
    /**
160
     * {@inheritDoc}
161
     */
162
    public function getPersisterKey()
163
    {
164
        return self::PERSISTER_KEY;
165
    }
166
167
    /**
168
     * {@inheritDoc}
169
     */
170
    public function getPolymorphicKey()
171
    {
172
        return self::POLYMORPHIC_KEY;
173
    }
174
175
    /**
176
     * @return Query
177
     */
178
    public function getQuery()
179
    {
180
        return $this->query;
181
    }
182
183
    /**
184
     * {@inheritDoc}
185
     */
186
    public function inverse(EntityMetadata $owner, EntityMetadata $rel, Store $store, array $identifiers, $inverseField)
187
    {
188
        $criteria = $this->getQuery()->getInverseCriteria($owner, $rel, $identifiers, $inverseField);
189
        $cursor = $this->getQuery()->executeFind($rel, $store, $criteria);
190
        return $this->getHydrator()->hydrateMany($rel, $cursor->toArray(), $store);
191
    }
192
193
    /**
194
     * {@inheritDoc}
195
     */
196
    public function query(EntityMetadata $metadata, Store $store, array $criteria, array $fields = [], array $sort = [], $offset = 0, $limit = 0)
197
    {
198
        $cursor = $this->getQuery()->executeFind($metadata, $store, $criteria);
199
        return $this->getHydrator()->hydrateMany($metadata, $cursor->toArray(), $store);
200
    }
201
202
    /**
203
     * {@inheritDoc}
204
     */
205
    public function retrieve(EntityMetadata $metadata, $identifier, Store $store)
206
    {
207
        $criteria = $this->getQuery()->getRetrieveCritiera($metadata, $identifier);
208
        $result = $this->getQuery()->executeFind($metadata, $store, $criteria)->getSingleResult();
209
        if (null === $result) {
210
            return;
211
        }
212
        return $this->getHydrator()->hydrateOne($metadata, $result, $store);
1 ignored issue
show
Bug introduced by
It seems like $result defined by $this->getQuery()->execu...ria)->getSingleResult() on line 208 can also be of type object; however, As3\Modlr\Persister\MongoDb\Hydrator::hydrateOne() 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...
213
    }
214
215
    /**
216
     * {@inheritDoc}
217
     * @todo    Optimize the changeset to query generation.
218
     */
219
    public function update(Model $model)
220
    {
221
        $metadata = $model->getMetadata();
222
        $criteria = $this->getQuery()->getRetrieveCritiera($metadata, $model->getId());
223
        $update = $this->createUpdateObj($model);
224
225
        if (empty($update)) {
226
            return $model;
227
        }
228
229
        $this->getQuery()->executeUpdate($metadata, $model->getStore(), $criteria, $update);
230
        return $model;
231
    }
232
233
    /**
234
     * Appends the change set values to a database object based on the provided handler.
235
     *
236
     * @param   Model   $model
237
     * @param   array   $obj
238
     * @param   Closure $handler
239
     * @return  array
240
     */
241
    private function appendChangeSet(Model $model, array $obj, Closure $handler)
242
    {
243
        $metadata = $model->getMetadata();
244
        $changeset = $model->getChangeSet();
245
        $formatter = $this->getFormatter();
246
247
        foreach ($this->changeSetMethods as $setKey => $methods) {
248
            list($metaMethod, $formatMethod) = $methods;
249
            foreach ($changeset[$setKey] as $key => $values) {
250
                $value = $formatter->$formatMethod($metadata->$metaMethod($key), $values['new']);
251
                $obj = $handler($key, $value, $obj);
252
            }
253
        }
254
        return $obj;
255
    }
256
257
    /**
258
     * Creates the database insert object for a Model.
259
     *
260
     * @param   Model   $model
261
     * @return  array
262
     */
263
    private function createInsertObj(Model $model)
264
    {
265
        $metadata = $model->getMetadata();
266
        $insert = [
267
            $this->getIdentifierKey()   => $this->convertId($model->getId()),
268
        ];
269
        if (true === $metadata->isChildEntity()) {
270
            $insert[$this->getPolymorphicKey()] = $metadata->type;
271
        }
272
        return $this->appendChangeSet($model, $insert, $this->getCreateChangeSetHandler());
273
    }
274
275
    /**
276
     * Creates the database update object for a Model.
277
     *
278
     * @param   Model   $model
279
     * @return  array
280
     */
281
    private function createUpdateObj(Model $model)
282
    {
283
        return $this->appendChangeSet($model, [], $this->getUpdateChangeSetHandler());
284
    }
285
286
    /**
287
     * Gets the change set handler Closure for create.
288
     *
289
     * @return  Closure
290
     */
291
    private function getCreateChangeSetHandler()
292
    {
293
        return function ($key, $value, $obj) {
294
            if (null !== $value) {
295
                $obj[$key] = $value;
296
            }
297
            return $obj;
298
        };
299
    }
300
301
    /**
302
     * Gets the change set handler Closure for update.
303
     *
304
     * @return  Closure
305
     */
306
    private function getUpdateChangeSetHandler()
307
    {
308
        return function ($key, $value, $obj) {
309
            $op = '$set';
310
            if (null === $value) {
311
                $op = '$unset';
312
                $value = 1;
313
            }
314
            $obj[$op][$key] = $value;
315
            return $obj;
316
        };
317
    }
318
}
319