Completed
Push — master ( 2fd33a...d63ace )
by Jacob
9s
created

Formatter::formatQueryExpression()   D

Complexity

Conditions 9
Paths 8

Size

Total Lines 36
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 2
Metric Value
c 3
b 1
f 2
dl 0
loc 36
rs 4.909
cc 9
eloc 21
nc 8
nop 2
1
<?php
2
3
namespace As3\Modlr\Persister\MongoDb;
4
5
use \Closure;
6
use \MongoId;
7
use As3\Modlr\Metadata\AttributeMetadata;
8
use As3\Modlr\Metadata\EmbeddedPropMetadata;
9
use As3\Modlr\Metadata\EntityMetadata;
10
use As3\Modlr\Metadata\RelationshipMetadata;
11
use As3\Modlr\Models\Embed;
12
use As3\Modlr\Models\Model;
13
use As3\Modlr\Persister\PersisterException;
14
use As3\Modlr\Store\Store;
15
16
/**
17
 * Handles persistence formatting operations for MongoDB.
18
 * - Formats query criteria to proper keys and values.
19
 * - Formats attribute, relationship, and embed values for insertion to the db.
20
 * - Formats identifier values for insertion to the db.
21
 *
22
 * @author Jacob Bare <[email protected]>
23
 */
24
final class Formatter
25
{
26
    /**
27
     * Query operators.
28
     * Organized into handling groups.
29
     *
30
     * @var array
31
     */
32
    private $ops = [
33
        'root'      => ['$and', '$or', '$nor'],
34
        'single'    => ['$eq', '$gt', '$gte', '$lt', '$lte', '$ne'],
35
        'multiple'  => ['$in', '$nin', '$all'],
36
        'recursive' => ['$not', '$elemMatch'],
37
        'ignore'    => ['$exists', '$type', '$mod', '$size', '$regex', '$text', '$where'],
38
    ];
39
40
    /**
41
     * Formats a set of query criteria for a Model.
42
     * Ensures the id and type fields are properly applied.
43
     * Ensures that values are properly converted to their database equivalents: e.g dates, mongo ids, etc.
44
     *
45
     * @param   EntityMetadata  $metadata
46
     * @param   Store           $store
47
     * @param   array           $criteria
48
     * @return  array
49
     */
50
    public function formatQuery(EntityMetadata $metadata, Store $store, array $criteria)
51
    {
52
        $formatted = [];
53
        foreach ($criteria as $key => $value) {
54
55
            if ($this->isOpType('root', $key) && is_array($value)) {
56
                foreach ($value as $subKey => $subValue) {
57
                    $formatted[$key][$subKey] = $this->formatQuery($metadata, $store, $subValue);
58
                }
59
                continue;
60
            }
61
62
            if ($this->isOperator($key) && is_array($value)) {
63
                $formatted[$key] = $this->formatQuery($metadata, $store, $value);
64
                continue;
65
            }
66
67
            list($key, $value) = $this->formatQueryElement($key, $value, $metadata, $store);
68
            $formatted[$key] = $value;
69
        }
70
        return $formatted;
71
    }
72
73
    /**
74
     * Prepares and formats an attribute value for proper insertion into the database.
75
     *
76
     * @param   AttributeMetadata   $attrMeta
77
     * @param   mixed               $value
78
     * @return  mixed
79
     */
80
    public function getAttributeDbValue(AttributeMetadata $attrMeta, $value)
81
    {
82
        // Handle data type conversion, if needed.
83
        if ('date' === $attrMeta->dataType && $value instanceof \DateTime) {
84
            return new \MongoDate($value->getTimestamp(), $value->format('u'));
85
        }
86
        if ('array' === $attrMeta->dataType && empty($value)) {
87
            return;
88
        }
89
        if ('object' === $attrMeta->dataType) {
90
            $array = (array) $value;
91
            return empty($array) ? null : $value;
92
        }
93
        return $value;
94
    }
95
96
    /**
97
     * Prepares and formats a has-many embed for proper insertion into the database.
98
     *
99
     * @param   EmbeddedPropMetadata    $embeddedPropMeta
100
     * @param   array|null              $embeds
101
     * @return  mixed
102
     */
103
    public function getEmbedManyDbValue(EmbeddedPropMetadata $embeddedPropMeta, array $embeds = null)
104
    {
105
        if (null === $embeds) {
106
            return;
107
        }
108
        $created = [];
109
        foreach ($embeds as $embed) {
110
            $created[] = $this->createEmbed($embeddedPropMeta, $embed);
111
        }
112
        return empty($created) ? null : $created;
113
    }
114
115
    /**
116
     * Prepares and formats a has-one embed for proper insertion into the database.
117
     *
118
     * @param   EmbeddedPropMetadata    $embeddedPropMeta
119
     * @param   Embed|null              $embed
120
     * @return  mixed
121
     */
122
    public function getEmbedOneDbValue(EmbeddedPropMetadata $embeddedPropMeta, Embed $embed = null)
123
    {
124
        if (null === $embed) {
125
            return;
126
        }
127
        return $this->createEmbed($embeddedPropMeta, $embed);
128
    }
129
130
    /**
131
     * Prepares and formats a has-one relationship model for proper insertion into the database.
132
     *
133
     * @param   RelationshipMetadata    $relMeta
134
     * @param   Model|null              $model
135
     * @return  mixed
136
     */
137
    public function getHasOneDbValue(RelationshipMetadata $relMeta, Model $model = null)
138
    {
139
        if (null === $model || true === $relMeta->isInverse) {
140
            return;
141
        }
142
        return $this->createReference($relMeta, $model);
143
    }
144
145
    /**
146
     * Prepares and formats a has-many relationship model set for proper insertion into the database.
147
     *
148
     * @param   RelationshipMetadata    $relMeta
149
     * @param   Model[]|null            $models
150
     * @return  mixed
151
     */
152
    public function getHasManyDbValue(RelationshipMetadata $relMeta, array $models = null)
153
    {
154
        if (null === $models || true === $relMeta->isInverse) {
155
            return null;
156
        }
157
        $references = [];
158
        foreach ($models as $model) {
159
            $references[] = $this->createReference($relMeta, $model);
160
        }
161
        return empty($references) ? null : $references;
162
    }
163
164
    /**
165
     * {@inheritDoc}
166
     */
167
    public function getIdentifierDbValue($identifier, $strategy = null)
168
    {
169
        if (false === $this->isIdStrategySupported($strategy)) {
170
            throw PersisterException::nyi('ID conversion currently only supports an object strategy, or none at all.');
171
        }
172
        if ($identifier instanceof MongoId) {
173
            return $identifier;
174
        }
175
        return new MongoId($identifier);
176
    }
177
178
    /**
179
     * Gets all possible identifier field keys (internal and persistence layer).
180
     *
181
     * @return  array
182
     */
183
    public function getIdentifierFields()
184
    {
185
        return [Persister::IDENTIFIER_KEY, EntityMetadata::ID_KEY];
186
    }
187
188
    /**
189
     * Gets all possible model type keys (internal and persistence layer).
190
     *
191
     * @return  array
192
     */
193
    public function getTypeFields()
194
    {
195
        return [Persister::POLYMORPHIC_KEY, EntityMetadata::TYPE_KEY];
196
    }
197
198
    /**
199
     * Determines if a field key is an identifier.
200
     * Uses both the internal and persistence identifier keys.
201
     *
202
     * @param   string  $key
203
     * @return  bool
204
     */
205
    public function isIdentifierField($key)
206
    {
207
        return in_array($key, $this->getIdentifierFields());
208
    }
209
210
    /**
211
     * Determines if the provided id strategy is supported.
212
     *
213
     * @param   string|null     $strategy
214
     * @return  bool
215
     */
216
    public function isIdStrategySupported($strategy)
217
    {
218
        return (null === $strategy || 'object' === $strategy);
219
    }
220
221
    /**
222
     * Determines if a field key is a model type field.
223
     * Uses both the internal and persistence model type keys.
224
     *
225
     * @param   string  $key
226
     * @return  bool
227
     */
228
    public function isTypeField($key)
229
    {
230
        return in_array($key, $this->getTypeFields());
231
    }
232
233
    /**
234
     * Creates an embed for storage of an embed model in the database
235
     *
236
     * @param   EmbeddedPropMetadata    $embeddedPropMeta
237
     * @param   Embed                   $embed
238
     * @return  array|null
239
     */
240
    private function createEmbed(EmbeddedPropMetadata $embeddedPropMeta, Embed $embed)
241
    {
242
        $embedMeta = $embeddedPropMeta->embedMeta;
243
244
        $obj = [];
245
        foreach ($embedMeta->getAttributes() as $key => $attrMeta) {
246
            $value = $this->getAttributeDbValue($attrMeta, $embed->get($key));
247
            if (null === $value) {
248
                continue;
249
            }
250
            $obj[$key] = $value;
251
        }
252
        foreach ($embedMeta->getEmbeds() as $key => $propMeta) {
253
            $value = (true === $propMeta->isOne()) ? $this->getEmbedOneDbValue($propMeta, $embed->get($key)) : $this->getEmbedManyDbValue($propMeta, $embed->get($key));
254
            if (null === $value) {
255
                continue;
256
            }
257
            $obj[$key] = $value;
258
        }
259
        return $obj;
260
    }
261
262
    /**
263
     * Creates a reference for storage of a related model in the database
264
     *
265
     * @param   RelationshipMetadata    $relMeta
266
     * @param   Model                   $model
267
     * @return  mixed
268
     */
269
    private function createReference(RelationshipMetadata $relMeta, Model $model)
270
    {
271
        $reference = [];
272
        $identifier = $this->getIdentifierDbValue($model->getId());
273
        if (true === $relMeta->isPolymorphic()) {
274
            $reference[Persister::IDENTIFIER_KEY] = $identifier;
275
            $reference[Persister::TYPE_KEY] = $model->getType();
276
            return $reference;
277
        }
278
        return $identifier;
279
    }
280
281
    /**
282
     * Formats a query element and ensures the correct key and value are set.
283
     * Returns a tuple of the formatted key and value.
284
     *
285
     * @param   string          $key
286
     * @param   mixed           $value
287
     * @param   EntityMetadata  $metadata
288
     * @param   Store           $store
289
     * @return  array
290
     */
291
    private function formatQueryElement($key, $value, EntityMetadata $metadata, Store $store)
292
    {
293
        // Handle root fields: id or model type.
294
        if (null !== $result = $this->formatQueryElementRoot($key, $value, $metadata)) {
295
            return $result;
296
        }
297
298
        // Handle attributes.
299
        if (null !== $result = $this->formatQueryElementAttr($key, $value, $metadata, $store)) {
300
            return $result;
301
        }
302
303
        // Handle relationships.
304
        if (null !== $result = $this->formatQueryElementRel($key, $value, $metadata, $store)) {
305
            return $result;
306
        }
307
308
        // Handle dot notated fields.
309
        if (null !== $result = $this->formatQueryElementDotted($key, $value, $metadata, $store)) {
310
            return $result;
311
        }
312
313
        // Pass remaining elements unconverted.
314
        return [$key, $value];
315
    }
316
317
    /**
318
     * Formats an attribute query element.
319
     * Returns a tuple of the formatted key and value, or null if the key is not an attribute field.
320
     *
321
     * @param   string          $key
322
     * @param   mixed           $value
323
     * @param   EntityMetadata  $metadata
324
     * @param   Store           $store
325
     * @return  array|null
326
     */
327
    private function formatQueryElementAttr($key, $value, EntityMetadata $metadata, Store $store)
328
    {
329
        if (null === $attrMeta = $metadata->getAttribute($key)) {
330
            return;
331
        }
332
333
        $converter = $this->getQueryAttrConverter($store, $attrMeta);
334
335
        if (is_array($value)) {
336
337
            if (true === $this->hasOperators($value)) {
338
                return [$key, $this->formatQueryExpression($value, $converter)];
339
            }
340
341
            if (in_array($attrMeta->dataType, ['array', 'object'])) {
342
                return [$key, $value];
343
            }
344
            return [$key, $this->formatQueryExpression(['$in' => $value], $converter)];
345
        }
346
        return [$key, $converter($value)];
347
    }
348
349
    /**
350
     * Formats a dot-notated field.
351
     * Returns a tuple of the formatted key and value, or null if the key is not a dot-notated field, or cannot be handled.
352
     *
353
     * @param   string          $key
354
     * @param   mixed           $value
355
     * @param   EntityMetadata  $metadata
356
     * @param   Store           $store
357
     * @return  array|null
358
     */
359
    private function formatQueryElementDotted($key, $value, EntityMetadata $metadata, Store $store)
360
    {
361
        if (false === stripos($key, '.')) {
362
            return;
363
        }
364
365
        $parts = explode('.', $key);
366
        $root = array_shift($parts);
367
        if (false === $metadata->hasRelationship($root)) {
368
            // Nothing to format. Allow the dotted field to pass normally.
369
            return [$key, $value];
370
        }
371
        $hasIndex = is_numeric($parts[0]);
372
373
        if (true === $hasIndex) {
374
            $subKey = isset($parts[1]) ? $parts[1] : 'id';
375
        } else {
376
            $subKey = $parts[0];
377
        }
378
379 View Code Duplication
        if ($this->isIdentifierField($subKey)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
380
            // Handle like a regular relationship
381
            list($key, $value) = $this->formatQueryElementRel($root, $value, $metadata, $store);
382
            $key = (true === $hasIndex) ? sprintf('%s.%s', $key, $parts[0]) : $key;
383
            return [$key, $value];
384
        }
385
386 View Code Duplication
        if ($this->isTypeField($subKey)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
387
            // Handle as a model type field.
388
            list($key, $value) = $this->formatQueryElementRoot($subKey, $value, $metadata);
389
            $key = (true === $hasIndex) ? sprintf('%s.%s.%s', $root, $parts[0], $key) : sprintf('%s.%s', $root, $key);
390
            return [$key, $value];
391
        }
392
        return [$key, $value];
393
    }
394
395
    /**
396
     * Formats a relationship query element.
397
     * Returns a tuple of the formatted key and value, or null if the key is not a relationship field.
398
     *
399
     * @param   string          $key
400
     * @param   mixed           $value
401
     * @param   EntityMetadata  $metadata
402
     * @param   Store           $store
403
     * @return  array|null
404
     */
405
    private function formatQueryElementRel($key, $value, EntityMetadata $metadata, Store $store)
406
    {
407
        if (null === $relMeta = $metadata->getRelationship($key)) {
408
            return;
409
        }
410
411
        $converter = $this->getQueryRelConverter($store, $relMeta);
412
413
        if (true === $relMeta->isPolymorphic()) {
414
            $key = sprintf('%s.%s', $key, Persister::IDENTIFIER_KEY);
415
        }
416
417 View Code Duplication
        if (is_array($value)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
418
            $value = (true === $this->hasOperators($value)) ? $value : ['$in' => $value];
419
            return [$key, $this->formatQueryExpression($value, $converter)];
420
        }
421
        return [$key, $converter($value)];
422
    }
423
424
425
    /**
426
     * Formats a root query element: either id or model type.
427
     * Returns a tuple of the formatted key and value, or null if the key is not a root field.
428
     *
429
     * @param   string          $key
430
     * @param   mixed           $value
431
     * @param   EntityMetadata  $metadata
432
     * @return  array|null
433
     */
434
    private function formatQueryElementRoot($key, $value, EntityMetadata $metadata)
435
    {
436
        if (true === $this->isIdentifierField($key)) {
437
            $dbKey = Persister::IDENTIFIER_KEY;
438
        } elseif (true === $this->isTypeField($key)) {
439
            $dbKey = Persister::POLYMORPHIC_KEY;
440
        } else {
441
            return;
442
        }
443
444
        $converter = $this->getQueryRootConverter($metadata, $dbKey);
445 View Code Duplication
        if (is_array($value)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
446
            $value = (true === $this->hasOperators($value)) ? $value : ['$in' => $value];
447
            return [$dbKey, $this->formatQueryExpression($value, $converter)];
448
        }
449
        return [$dbKey, $converter($value)];
450
    }
451
452
    /**
453
     * Formats a query expression.
454
     *
455
     * @param   array   $expression
456
     * @param   Closure $converter
457
     * @return  array
458
     */
459
    private function formatQueryExpression(array $expression, Closure $converter)
460
    {
461
        foreach ($expression as $key => $value) {
462
463
            if ('$regex' === $key && !$value instanceof \MongoRegex) {
464
                $expression[$key] = new \MongoRegex($value);
465
                continue;
466
            }
467
468
            if (true === $this->isOpType('ignore', $key)) {
469
                continue;
470
            }
471
472
            if (true === $this->isOpType('single', $key)) {
473
                $expression[$key] = $converter($value);
474
                continue;
475
            }
476
477
            if (true === $this->isOpType('multiple', $key)) {
478
                $value = (array) $value;
479
                foreach ($value as $subKey => $subValue) {
480
                    $expression[$key][$subKey] = $converter($subValue);
481
                }
482
                continue;
483
            }
484
485
            if (true === $this->isOpType('recursive', $key)) {
486
                $value = (array) $value;
487
                $expression[$key] = $this->formatQueryExpression($value, $converter);
488
                continue;
489
            }
490
491
            $expression[$key] = $converter($value);
492
        }
493
        return $expression;
494
    }
495
496
    /**
497
     * Gets the converter for handling attribute values in queries.
498
     *
499
     * @param   Store               $store
500
     * @param   AttributeMetadata   $attrMeta
501
     * @return  Closure
502
     */
503
    private function getQueryAttrConverter(Store $store, AttributeMetadata $attrMeta)
504
    {
505
        return function ($value) use ($store, $attrMeta) {
506
            if (in_array($attrMeta->dataType, ['object', 'array'])) {
507
                // Leave the value as is.
508
                return $value;
509
            }
510
            $value = $store->convertAttributeValue($attrMeta->dataType, $value);
511
            return $this->getAttributeDbValue($attrMeta, $value);
512
        };
513
514
    }
515
516
    /**
517
     * Gets the converter for handling relationship values in queries.
518
     *
519
     * @param   Store                   $store
520
     * @param   RelationshipMetadata    $relMeta
521
     * @return  Closure
522
     */
523
    private function getQueryRelConverter(Store $store, RelationshipMetadata $relMeta)
524
    {
525
        $related = $store->getMetadataForType($relMeta->getEntityType());
526
        return $this->getQueryRootConverter($related, Persister::IDENTIFIER_KEY);
527
    }
528
529
    /**
530
     * Gets the converter for handling root field values in queries (id or model type).
531
     *
532
     * @param   EntityMetadata  $metadata
533
     * @param   string          $key
534
     * @return  Closure
535
     */
536
    private function getQueryRootConverter(EntityMetadata $metadata, $key)
537
    {
538
        return function($value) use ($metadata, $key) {
539
            if ($key === Persister::POLYMORPHIC_KEY) {
540
                return $value;
541
            }
542
            $strategy = ($metadata->persistence instanceof StorageMetadata) ? $metadata->persistence->idStrategy : null;
543
            return $this->getIdentifierDbValue($value, $strategy);
544
        };
545
    }
546
547
    /**
548
     * Determines whether a query value has additional query operators.
549
     *
550
     * @param   mixed   $value
551
     * @return  bool
552
     */
553
    private function hasOperators($value)
554
    {
555
556
        if (!is_array($value) && !is_object($value)) {
557
            return false;
558
        }
559
560
        if (is_object($value)) {
561
            $value = get_object_vars($value);
562
        }
563
564
        foreach ($value as $key => $subValue) {
565
            if (true === $this->isOperator($key)) {
566
                return true;
567
            }
568
        }
569
        return false;
570
    }
571
572
    /**
573
     * Determines if a key is a query operator.
574
     *
575
     * @param   string  $key
576
     * @return  bool
577
     */
578
    private function isOperator($key)
579
    {
580
        return isset($key[0]) && '$' === $key[0];
581
    }
582
583
    /**
584
     * Determines if a key is of a certain operator handling type.
585
     *
586
     * @param   string  $type
587
     * @param   string  $key
588
     * @return  bool
589
     */
590
    private function isOpType($type, $key)
591
    {
592
        if (!isset($this->ops[$type])) {
593
            return false;
594
        }
595
        return in_array($key, $this->ops[$type]);
596
    }
597
}
598