1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace As3\Modlr\Persister\MongoDb; |
4
|
|
|
|
5
|
|
|
use As3\Modlr\Store\Store; |
6
|
|
|
use As3\Modlr\Models\Model; |
7
|
|
|
use As3\Modlr\Models\Collection; |
8
|
|
|
use As3\Modlr\Metadata\EntityMetadata; |
9
|
|
|
use As3\Modlr\Metadata\AttributeMetadata; |
10
|
|
|
use As3\Modlr\Metadata\RelationshipMetadata; |
11
|
|
|
use As3\Modlr\Persister\PersisterInterface; |
12
|
|
|
use As3\Modlr\Persister\PersisterException; |
13
|
|
|
use As3\Modlr\Persister\Record; |
14
|
|
|
use Doctrine\MongoDB\Connection; |
15
|
|
|
use \MongoId; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Persists and retrieves models to/from a MongoDB database. |
19
|
|
|
* |
20
|
|
|
* @author Jacob Bare <[email protected]> |
21
|
|
|
*/ |
22
|
|
|
final class Persister implements PersisterInterface |
23
|
|
|
{ |
24
|
|
|
const IDENTIFIER_KEY = '_id'; |
25
|
|
|
const POLYMORPHIC_KEY = '_type'; |
26
|
|
|
const PERSISTER_KEY = 'mongodb'; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* The Doctine MongoDB connection. |
30
|
|
|
* |
31
|
|
|
* @var Connection |
32
|
|
|
*/ |
33
|
|
|
private $connection; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* The query/database operations formatter. |
37
|
|
|
* |
38
|
|
|
* @var Formatter |
39
|
|
|
*/ |
40
|
|
|
private $formatter; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* The raw result hydrator. |
44
|
|
|
* |
45
|
|
|
* @var Hydrator |
46
|
|
|
*/ |
47
|
|
|
private $hydrator; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* @var StorageMetadataFactory |
51
|
|
|
*/ |
52
|
|
|
private $smf; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Constructor. |
56
|
|
|
* |
57
|
|
|
* @param Connection $connection |
58
|
|
|
* @param StorageMetadataFactory $smf |
59
|
|
|
*/ |
60
|
|
|
public function __construct(Connection $connection, StorageMetadataFactory $smf) |
61
|
|
|
{ |
62
|
|
|
$this->connection = $connection; |
63
|
|
|
$this->formatter = new Formatter(); |
64
|
|
|
$this->hydrator = new Hydrator(); |
65
|
|
|
$this->smf = $smf; |
66
|
|
|
|
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* {@inheritDoc} |
71
|
|
|
*/ |
72
|
|
|
public function getPersisterKey() |
73
|
|
|
{ |
74
|
|
|
return self::PERSISTER_KEY; |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* {@inheritDoc} |
79
|
|
|
*/ |
80
|
|
|
public function getPersistenceMetadataFactory() |
81
|
|
|
{ |
82
|
|
|
return $this->smf; |
|
|
|
|
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* {@inheritDoc} |
87
|
|
|
* @todo Implement sorting and pagination (limit/skip). |
88
|
|
|
*/ |
89
|
|
|
public function all(EntityMetadata $metadata, Store $store, array $identifiers = []) |
90
|
|
|
{ |
91
|
|
|
$criteria = $this->getRetrieveCritiera($metadata, $identifiers); |
92
|
|
|
$cursor = $this->doQuery($metadata, $store, $criteria); |
93
|
|
|
return $this->getHydrator()->hydrateMany($metadata, $cursor->toArray(), $store); |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* {@inheritDoc} |
98
|
|
|
*/ |
99
|
|
|
public function query(EntityMetadata $metadata, Store $store, array $criteria, array $fields = [], array $sort = [], $offset = 0, $limit = 0) |
100
|
|
|
{ |
101
|
|
|
$cursor = $this->doQuery($metadata, $store, $criteria); |
102
|
|
|
return $this->getHydrator()->hydrateMany($metadata, $cursor->toArray(), $store); |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* {@inheritDoc} |
107
|
|
|
*/ |
108
|
|
|
public function inverse(EntityMetadata $owner, EntityMetadata $rel, Store $store, array $identifiers, $inverseField) |
109
|
|
|
{ |
110
|
|
|
$criteria = $this->getInverseCriteria($owner, $rel, $identifiers, $inverseField); |
111
|
|
|
$cursor = $this->doQuery($rel, $store, $criteria); |
112
|
|
|
return $this->getHydrator()->hydrateMany($rel, $cursor->toArray(), $store); |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
/** |
116
|
|
|
* {@inheritDoc} |
117
|
|
|
*/ |
118
|
|
|
public function retrieve(EntityMetadata $metadata, $identifier, Store $store) |
119
|
|
|
{ |
120
|
|
|
$criteria = $this->getRetrieveCritiera($metadata, $identifier); |
121
|
|
|
$result = $this->doQuery($metadata, $store, $criteria)->getSingleResult(); |
122
|
|
|
if (null === $result) { |
123
|
|
|
return; |
124
|
|
|
} |
125
|
|
|
return $this->getHydrator()->hydrateOne($metadata, $result, $store); |
|
|
|
|
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* {@inheritDoc} |
130
|
|
|
* @todo Optimize the changeset to query generation. |
131
|
|
|
*/ |
132
|
|
|
public function create(Model $model) |
133
|
|
|
{ |
134
|
|
|
$metadata = $model->getMetadata(); |
135
|
|
|
$insert[$this->getIdentifierKey()] = $this->convertId($model->getId()); |
|
|
|
|
136
|
|
|
if (true === $metadata->isChildEntity()) { |
137
|
|
|
$insert[$this->getPolymorphicKey()] = $metadata->type; |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
$changeset = $model->getChangeSet(); |
141
|
|
View Code Duplication |
foreach ($changeset['attributes'] as $key => $values) { |
|
|
|
|
142
|
|
|
$value = $this->getFormatter()->getAttributeDbValue($metadata->getAttribute($key), $values['new']); |
|
|
|
|
143
|
|
|
if (null === $value) { |
144
|
|
|
continue; |
145
|
|
|
} |
146
|
|
|
$insert[$key] = $value; |
147
|
|
|
} |
148
|
|
View Code Duplication |
foreach ($changeset['hasOne'] as $key => $values) { |
|
|
|
|
149
|
|
|
$value = $this->getFormatter()->getHasOneDbValue($metadata->getRelationship($key), $values['new']); |
|
|
|
|
150
|
|
|
if (null === $value) { |
151
|
|
|
continue; |
152
|
|
|
} |
153
|
|
|
$insert[$key] = $value; |
154
|
|
|
} |
155
|
|
View Code Duplication |
foreach ($changeset['hasMany'] as $key => $values) { |
|
|
|
|
156
|
|
|
$value = $this->getFormatter()->getHasManyDbValue($metadata->getRelationship($key), $values['new']); |
|
|
|
|
157
|
|
|
if (null === $value) { |
158
|
|
|
continue; |
159
|
|
|
} |
160
|
|
|
$insert[$key] = $value; |
161
|
|
|
} |
162
|
|
|
$this->createQueryBuilder($metadata) |
163
|
|
|
->insert() |
164
|
|
|
->setNewObj($insert) |
165
|
|
|
->getQuery() |
166
|
|
|
->execute() |
167
|
|
|
; |
168
|
|
|
return $model; |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
/** |
172
|
|
|
* {@inheritDoc} |
173
|
|
|
* @todo Optimize the changeset to query generation. |
174
|
|
|
*/ |
175
|
|
|
public function update(Model $model) |
176
|
|
|
{ |
177
|
|
|
$metadata = $model->getMetadata(); |
178
|
|
|
$criteria = $this->getRetrieveCritiera($metadata, $model->getId()); |
179
|
|
|
$changeset = $model->getChangeSet(); |
180
|
|
|
|
181
|
|
|
$update = []; |
182
|
|
View Code Duplication |
foreach ($changeset['attributes'] as $key => $values) { |
|
|
|
|
183
|
|
|
if (null === $values['new']) { |
184
|
|
|
$op = '$unset'; |
185
|
|
|
$value = 1; |
186
|
|
|
} else { |
187
|
|
|
$op = '$set'; |
188
|
|
|
$value = $this->getFormatter()->getAttributeDbValue($metadata->getAttribute($key), $values['new']); |
|
|
|
|
189
|
|
|
} |
190
|
|
|
$update[$op][$key] = $value; |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
// @todo Must prevent inverse relationships from persisting |
194
|
|
View Code Duplication |
foreach ($changeset['hasOne'] as $key => $values) { |
|
|
|
|
195
|
|
|
if (null === $values['new']) { |
196
|
|
|
$op = '$unset'; |
197
|
|
|
$value = 1; |
198
|
|
|
} else { |
199
|
|
|
$op = '$set'; |
200
|
|
|
$value = $this->getFormatter()->getHasOneDbValue($metadata->getRelationship($key), $values['new']); |
|
|
|
|
201
|
|
|
} |
202
|
|
|
$update[$op][$key] = $value; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
View Code Duplication |
foreach ($changeset['hasMany'] as $key => $values) { |
|
|
|
|
206
|
|
|
if (null === $values['new']) { |
207
|
|
|
$op = '$unset'; |
208
|
|
|
$value = 1; |
209
|
|
|
} else { |
210
|
|
|
$op = '$set'; |
211
|
|
|
$value = $this->getFormatter()->getHasManyDbValue($metadata->getRelationship($key), $values['new']); |
|
|
|
|
212
|
|
|
} |
213
|
|
|
$update[$op][$key] = $value; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
if (empty($update)) { |
217
|
|
|
return $model; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
$this->createQueryBuilder($metadata) |
221
|
|
|
->update() |
222
|
|
|
->setQueryArray($criteria) |
223
|
|
|
->setNewObj($update) |
224
|
|
|
->getQuery() |
225
|
|
|
->execute(); |
226
|
|
|
; |
227
|
|
|
return $model; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* {@inheritDoc} |
232
|
|
|
*/ |
233
|
|
|
public function delete(Model $model) |
234
|
|
|
{ |
235
|
|
|
$metadata = $model->getMetadata(); |
236
|
|
|
$criteria = $this->getRetrieveCritiera($metadata, $model->getId()); |
237
|
|
|
|
238
|
|
|
$this->createQueryBuilder($metadata) |
239
|
|
|
->remove() |
240
|
|
|
->setQueryArray($criteria) |
241
|
|
|
->getQuery() |
242
|
|
|
->execute(); |
243
|
|
|
; |
244
|
|
|
return $model; |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
/** |
248
|
|
|
* {@inheritDoc} |
249
|
|
|
*/ |
250
|
|
|
public function generateId($strategy = null) |
251
|
|
|
{ |
252
|
|
|
if (false === $this->getFormatter()->isIdStrategySupported($strategy)) { |
253
|
|
|
throw PersisterException::nyi('ID generation currently only supports an object strategy, or none at all.'); |
254
|
|
|
} |
255
|
|
|
return new MongoId(); |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
/** |
259
|
|
|
* @return Formatter |
260
|
|
|
*/ |
261
|
|
|
public function getFormatter() |
262
|
|
|
{ |
263
|
|
|
return $this->formatter; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* @return Hydrator |
268
|
|
|
*/ |
269
|
|
|
public function getHydrator() |
270
|
|
|
{ |
271
|
|
|
return $this->hydrator; |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
/** |
275
|
|
|
* {@inheritDoc} |
276
|
|
|
*/ |
277
|
|
|
public function convertId($identifier, $strategy = null) |
278
|
|
|
{ |
279
|
|
|
return $this->getFormatter()->getIdentifierDbValue($identifier, $strategy); |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
/** |
283
|
|
|
* {@inheritDoc} |
284
|
|
|
*/ |
285
|
|
|
public function getIdentifierKey() |
286
|
|
|
{ |
287
|
|
|
return self::IDENTIFIER_KEY; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
/** |
291
|
|
|
* {@inheritDoc} |
292
|
|
|
*/ |
293
|
|
|
public function getPolymorphicKey() |
294
|
|
|
{ |
295
|
|
|
return self::POLYMORPHIC_KEY; |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
/** |
299
|
|
|
* {@inheritDoc} |
300
|
|
|
*/ |
301
|
|
|
public function extractType(EntityMetadata $metadata, array $data) |
302
|
|
|
{ |
303
|
|
|
return $this->getHydrator()->extractType($metadata, $data); |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
/** |
307
|
|
|
* Finds records from the database based on the provided metadata and criteria. |
308
|
|
|
* |
309
|
|
|
* @param EntityMetadata $metadata The model metadata that the database should query against. |
310
|
|
|
* @param Store $store The store. |
311
|
|
|
* @param array $criteria The query criteria. |
312
|
|
|
* @param array $fields Fields to include/exclude. |
313
|
|
|
* @param array $sort The sort criteria. |
314
|
|
|
* @param int $offset The starting offset, aka the number of Models to skip. |
315
|
|
|
* @param int $limit The number of Models to limit. |
316
|
|
|
* @return \Doctrine\MongoDB\Cursor |
317
|
|
|
*/ |
318
|
|
|
protected function doQuery(EntityMetadata $metadata, Store $store, array $criteria, array $fields = [], array $sort = [], $offset = 0, $limit = 0) |
|
|
|
|
319
|
|
|
{ |
320
|
|
|
$criteria = $this->getFormatter()->formatQuery($metadata, $store, $criteria); |
321
|
|
|
return $this->createQueryBuilder($metadata) |
322
|
|
|
->find() |
323
|
|
|
->setQueryArray($criteria) |
324
|
|
|
->getQuery() |
325
|
|
|
->execute() |
326
|
|
|
; |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
/** |
330
|
|
|
* Gets standard database retrieval criteria for an inverse relationship. |
331
|
|
|
* |
332
|
|
|
* @param EntityMetadata $metadata The entity to retrieve database records for. |
|
|
|
|
333
|
|
|
* @param string|array $identifiers The IDs to query. |
334
|
|
|
* @return array |
335
|
|
|
*/ |
336
|
|
|
protected function getInverseCriteria(EntityMetadata $owner, EntityMetadata $related, $identifiers, $inverseField) |
337
|
|
|
{ |
338
|
|
|
$criteria[$inverseField] = (array) $identifiers; |
|
|
|
|
339
|
|
|
if (true === $owner->isChildEntity()) { |
340
|
|
|
// The owner is owned by a polymorphic model. Must include the type with the inverse field criteria. |
341
|
|
|
$criteria[$inverseField] = [ |
342
|
|
|
$this->getIdentifierKey() => $criteria[$inverseField], |
343
|
|
|
$this->getPolymorphicKey() => $owner->type, |
344
|
|
|
]; |
345
|
|
|
} |
346
|
|
|
if (true === $related->isChildEntity()) { |
347
|
|
|
// The relationship is owned by a polymorphic model. Must include the type in the root criteria. |
348
|
|
|
$criteria[$this->getPolymorphicKey()] = $related->type; |
349
|
|
|
} |
350
|
|
|
return $criteria; |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
/** |
354
|
|
|
* Gets standard database retrieval criteria for an entity and the provided identifiers. |
355
|
|
|
* |
356
|
|
|
* @param EntityMetadata $metadata The entity to retrieve database records for. |
357
|
|
|
* @param string|array|null $identifiers The IDs to query. |
358
|
|
|
* @return array |
359
|
|
|
*/ |
360
|
|
|
protected function getRetrieveCritiera(EntityMetadata $metadata, $identifiers = null) |
361
|
|
|
{ |
362
|
|
|
$criteria = []; |
363
|
|
|
if (true === $metadata->isChildEntity()) { |
364
|
|
|
$criteria[$this->getPolymorphicKey()] = $metadata->type; |
365
|
|
|
} |
366
|
|
|
|
367
|
|
|
if (null === $identifiers) { |
368
|
|
|
return $criteria; |
369
|
|
|
} |
370
|
|
|
$identifiers = (array) $identifiers; |
371
|
|
|
if (empty($identifiers)) { |
372
|
|
|
return $criteria; |
373
|
|
|
} |
374
|
|
|
$criteria[$this->getIdentifierKey()] = (1 === count($identifiers)) ? $identifiers[0] : $identifiers; |
375
|
|
|
return $criteria; |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
/** |
379
|
|
|
* Creates a builder object for querying MongoDB based on the provided metadata. |
380
|
|
|
* |
381
|
|
|
* @param EntityMetadata $metadata |
382
|
|
|
* @return \Doctrine\MongoDB\Query\Builder |
383
|
|
|
*/ |
384
|
|
|
protected function createQueryBuilder(EntityMetadata $metadata) |
385
|
|
|
{ |
386
|
|
|
return $this->getModelCollection($metadata)->createQueryBuilder(); |
387
|
|
|
} |
388
|
|
|
|
389
|
|
|
/** |
390
|
|
|
* Gets the MongoDB Collection object for a Model. |
391
|
|
|
* |
392
|
|
|
* @param EntityMetadata $metadata |
393
|
|
|
* @return \Doctrine\MongoDB\Collection |
394
|
|
|
*/ |
395
|
|
|
protected function getModelCollection(EntityMetadata $metadata) |
396
|
|
|
{ |
397
|
|
|
return $this->connection->selectCollection($metadata->persistence->db, $metadata->persistence->collection); |
|
|
|
|
398
|
|
|
} |
399
|
|
|
} |
400
|
|
|
|
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:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.