Formatter   D
last analyzed

Complexity

Total Complexity 101

Size/Duplication

Total Lines 578
Duplicated Lines 3.46 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 0
Metric Value
wmc 101
lcom 1
cbo 8
dl 20
loc 578
rs 4.8717
c 0
b 0
f 0

26 Methods

Rating   Name   Duplication   Size   Complexity  
C formatQuery() 0 22 7
B getAttributeDbValue() 0 15 7
B getEmbedManyDbValue() 0 15 6
A getEmbedOneDbValue() 0 7 2
A getHasOneDbValue() 0 7 3
B getHasManyDbValue() 0 11 5
A getIdentifierDbValue() 0 10 3
A getIdentifierFields() 0 4 1
A getTypeFields() 0 4 1
A isIdentifierField() 0 4 1
A isIdStrategySupported() 0 4 2
A isTypeField() 0 4 1
B createEmbed() 0 21 6
A createReference() 0 11 2
B formatQueryElement() 0 25 5
B formatQueryElementAttr() 0 21 5
D formatQueryElementDotted() 12 35 9
B formatQueryElementRel() 4 18 5
B formatQueryElementRoot() 4 17 5
D formatQueryExpression() 0 36 9
A getQueryAttrConverter() 0 12 2
A getQueryRelConverter() 0 5 1
A getQueryRootConverter() 0 10 3
B hasOperators() 0 18 6
A isOperator() 0 4 2
A isOpType() 0 7 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Formatter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Formatter, and based on these observations, apply Extract Interface, too.

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   EmbedCollection|array|null  $embeds
101
     * @return  mixed
102
     */
103
    public function getEmbedManyDbValue(EmbeddedPropMetadata $embeddedPropMeta, $embeds = null)
104
    {
105
        if (null === $embeds) {
106
            return;
107
        }
108
        if (!is_array($embeds) && !$embeds instanceof \Traversable) {
109
            throw new \InvalidArgumentException('Unable to traverse the embed collection');
110
        }
111
112
        $created = [];
113
        foreach ($embeds as $embed) {
114
            $created[] = $this->createEmbed($embeddedPropMeta, $embed);
115
        }
116
        return empty($created) ? null : $created;
117
    }
118
119
    /**
120
     * Prepares and formats a has-one embed for proper insertion into the database.
121
     *
122
     * @param   EmbeddedPropMetadata    $embeddedPropMeta
123
     * @param   Embed|null              $embed
124
     * @return  mixed
125
     */
126
    public function getEmbedOneDbValue(EmbeddedPropMetadata $embeddedPropMeta, Embed $embed = null)
127
    {
128
        if (null === $embed) {
129
            return;
130
        }
131
        return $this->createEmbed($embeddedPropMeta, $embed);
132
    }
133
134
    /**
135
     * Prepares and formats a has-one relationship model for proper insertion into the database.
136
     *
137
     * @param   RelationshipMetadata    $relMeta
138
     * @param   Model|null              $model
139
     * @return  mixed
140
     */
141
    public function getHasOneDbValue(RelationshipMetadata $relMeta, Model $model = null)
142
    {
143
        if (null === $model || true === $relMeta->isInverse) {
144
            return;
145
        }
146
        return $this->createReference($relMeta, $model);
147
    }
148
149
    /**
150
     * Prepares and formats a has-many relationship model set for proper insertion into the database.
151
     *
152
     * @param   RelationshipMetadata    $relMeta
153
     * @param   Model[]|null            $models
154
     * @return  mixed
155
     */
156
    public function getHasManyDbValue(RelationshipMetadata $relMeta, array $models = null)
157
    {
158
        if (null === $models || true === $relMeta->isInverse) {
159
            return null;
160
        }
161
        $references = [];
162
        foreach ($models as $model) {
163
            $references[] = $this->createReference($relMeta, $model);
164
        }
165
        return empty($references) ? null : $references;
166
    }
167
168
    /**
169
     * {@inheritDoc}
170
     */
171
    public function getIdentifierDbValue($identifier, $strategy = null)
172
    {
173
        if (false === $this->isIdStrategySupported($strategy)) {
174
            throw PersisterException::nyi('ID conversion currently only supports an object strategy, or none at all.');
175
        }
176
        if ($identifier instanceof MongoId) {
177
            return $identifier;
178
        }
179
        return new MongoId($identifier);
180
    }
181
182
    /**
183
     * Gets all possible identifier field keys (internal and persistence layer).
184
     *
185
     * @return  array
186
     */
187
    public function getIdentifierFields()
188
    {
189
        return [Persister::IDENTIFIER_KEY, EntityMetadata::ID_KEY];
190
    }
191
192
    /**
193
     * Gets all possible model type keys (internal and persistence layer).
194
     *
195
     * @return  array
196
     */
197
    public function getTypeFields()
198
    {
199
        return [Persister::POLYMORPHIC_KEY, EntityMetadata::TYPE_KEY];
200
    }
201
202
    /**
203
     * Determines if a field key is an identifier.
204
     * Uses both the internal and persistence identifier keys.
205
     *
206
     * @param   string  $key
207
     * @return  bool
208
     */
209
    public function isIdentifierField($key)
210
    {
211
        return in_array($key, $this->getIdentifierFields());
212
    }
213
214
    /**
215
     * Determines if the provided id strategy is supported.
216
     *
217
     * @param   string|null     $strategy
218
     * @return  bool
219
     */
220
    public function isIdStrategySupported($strategy)
221
    {
222
        return (null === $strategy || 'object' === $strategy);
223
    }
224
225
    /**
226
     * Determines if a field key is a model type field.
227
     * Uses both the internal and persistence model type keys.
228
     *
229
     * @param   string  $key
230
     * @return  bool
231
     */
232
    public function isTypeField($key)
233
    {
234
        return in_array($key, $this->getTypeFields());
235
    }
236
237
    /**
238
     * Creates an embed for storage of an embed model in the database
239
     *
240
     * @param   EmbeddedPropMetadata    $embeddedPropMeta
241
     * @param   Embed                   $embed
242
     * @return  array|null
243
     */
244
    private function createEmbed(EmbeddedPropMetadata $embeddedPropMeta, Embed $embed)
245
    {
246
        $embedMeta = $embeddedPropMeta->embedMeta;
247
248
        $obj = [];
249
        foreach ($embedMeta->getAttributes() as $key => $attrMeta) {
250
            $value = $this->getAttributeDbValue($attrMeta, $embed->get($key));
251
            if (null === $value) {
252
                continue;
253
            }
254
            $obj[$key] = $value;
255
        }
256
        foreach ($embedMeta->getEmbeds() as $key => $propMeta) {
257
            $value = (true === $propMeta->isOne()) ? $this->getEmbedOneDbValue($propMeta, $embed->get($key)) : $this->getEmbedManyDbValue($propMeta, $embed->get($key));
258
            if (null === $value) {
259
                continue;
260
            }
261
            $obj[$key] = $value;
262
        }
263
        return $obj;
264
    }
265
266
    /**
267
     * Creates a reference for storage of a related model in the database
268
     *
269
     * @param   RelationshipMetadata    $relMeta
270
     * @param   Model                   $model
271
     * @return  mixed
272
     */
273
    private function createReference(RelationshipMetadata $relMeta, Model $model)
274
    {
275
        $reference = [];
276
        $identifier = $this->getIdentifierDbValue($model->getId());
277
        if (true === $relMeta->isPolymorphic()) {
278
            $reference[Persister::IDENTIFIER_KEY]  = $identifier;
279
            $reference[Persister::POLYMORPHIC_KEY] = $model->getType();
280
            return $reference;
281
        }
282
        return $identifier;
283
    }
284
285
    /**
286
     * Formats a query element and ensures the correct key and value are set.
287
     * Returns a tuple of the formatted key and value.
288
     *
289
     * @param   string          $key
290
     * @param   mixed           $value
291
     * @param   EntityMetadata  $metadata
292
     * @param   Store           $store
293
     * @return  array
294
     */
295
    private function formatQueryElement($key, $value, EntityMetadata $metadata, Store $store)
296
    {
297
        // Handle root fields: id or model type.
298
        if (null !== $result = $this->formatQueryElementRoot($key, $value, $metadata)) {
299
            return $result;
300
        }
301
302
        // Handle attributes.
303
        if (null !== $result = $this->formatQueryElementAttr($key, $value, $metadata, $store)) {
304
            return $result;
305
        }
306
307
        // Handle relationships.
308
        if (null !== $result = $this->formatQueryElementRel($key, $value, $metadata, $store)) {
309
            return $result;
310
        }
311
312
        // Handle dot notated fields.
313
        if (null !== $result = $this->formatQueryElementDotted($key, $value, $metadata, $store)) {
314
            return $result;
315
        }
316
317
        // Pass remaining elements unconverted.
318
        return [$key, $value];
319
    }
320
321
    /**
322
     * Formats an attribute query element.
323
     * Returns a tuple of the formatted key and value, or null if the key is not an attribute field.
324
     *
325
     * @param   string          $key
326
     * @param   mixed           $value
327
     * @param   EntityMetadata  $metadata
328
     * @param   Store           $store
329
     * @return  array|null
330
     */
331
    private function formatQueryElementAttr($key, $value, EntityMetadata $metadata, Store $store)
332
    {
333
        if (null === $attrMeta = $metadata->getAttribute($key)) {
334
            return;
335
        }
336
337
        $converter = $this->getQueryAttrConverter($store, $attrMeta);
338
339
        if (is_array($value)) {
340
341
            if (true === $this->hasOperators($value)) {
342
                return [$key, $this->formatQueryExpression($value, $converter)];
343
            }
344
345
            if (in_array($attrMeta->dataType, ['array', 'object'])) {
346
                return [$key, $value];
347
            }
348
            return [$key, $this->formatQueryExpression(['$in' => $value], $converter)];
349
        }
350
        return [$key, $converter($value)];
351
    }
352
353
    /**
354
     * Formats a dot-notated field.
355
     * Returns a tuple of the formatted key and value, or null if the key is not a dot-notated field, or cannot be handled.
356
     *
357
     * @param   string          $key
358
     * @param   mixed           $value
359
     * @param   EntityMetadata  $metadata
360
     * @param   Store           $store
361
     * @return  array|null
362
     */
363
    private function formatQueryElementDotted($key, $value, EntityMetadata $metadata, Store $store)
364
    {
365
        if (false === stripos($key, '.')) {
366
            return;
367
        }
368
369
        $parts = explode('.', $key);
370
        $root = array_shift($parts);
371
        if (false === $metadata->hasRelationship($root)) {
372
            // Nothing to format. Allow the dotted field to pass normally.
373
            return [$key, $value];
374
        }
375
        $hasIndex = is_numeric($parts[0]);
376
377
        if (true === $hasIndex) {
378
            $subKey = isset($parts[1]) ? $parts[1] : 'id';
379
        } else {
380
            $subKey = $parts[0];
381
        }
382
383 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...
384
            // Handle like a regular relationship
385
            list($key, $value) = $this->formatQueryElementRel($root, $value, $metadata, $store);
386
            $key = (true === $hasIndex) ? sprintf('%s.%s', $key, $parts[0]) : $key;
387
            return [$key, $value];
388
        }
389
390 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...
391
            // Handle as a model type field.
392
            list($key, $value) = $this->formatQueryElementRoot($subKey, $value, $metadata);
393
            $key = (true === $hasIndex) ? sprintf('%s.%s.%s', $root, $parts[0], $key) : sprintf('%s.%s', $root, $key);
394
            return [$key, $value];
395
        }
396
        return [$key, $value];
397
    }
398
399
    /**
400
     * Formats a relationship query element.
401
     * Returns a tuple of the formatted key and value, or null if the key is not a relationship field.
402
     *
403
     * @param   string          $key
404
     * @param   mixed           $value
405
     * @param   EntityMetadata  $metadata
406
     * @param   Store           $store
407
     * @return  array|null
408
     */
409
    private function formatQueryElementRel($key, $value, EntityMetadata $metadata, Store $store)
410
    {
411
        if (null === $relMeta = $metadata->getRelationship($key)) {
412
            return;
413
        }
414
415
        $converter = $this->getQueryRelConverter($store, $relMeta);
416
417
        if (true === $relMeta->isPolymorphic()) {
418
            $key = sprintf('%s.%s', $key, Persister::IDENTIFIER_KEY);
419
        }
420
421 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...
422
            $value = (true === $this->hasOperators($value)) ? $value : ['$in' => $value];
423
            return [$key, $this->formatQueryExpression($value, $converter)];
424
        }
425
        return [$key, $converter($value)];
426
    }
427
428
429
    /**
430
     * Formats a root query element: either id or model type.
431
     * Returns a tuple of the formatted key and value, or null if the key is not a root field.
432
     *
433
     * @param   string          $key
434
     * @param   mixed           $value
435
     * @param   EntityMetadata  $metadata
436
     * @return  array|null
437
     */
438
    private function formatQueryElementRoot($key, $value, EntityMetadata $metadata)
439
    {
440
        if (true === $this->isIdentifierField($key)) {
441
            $dbKey = Persister::IDENTIFIER_KEY;
442
        } elseif (true === $this->isTypeField($key)) {
443
            $dbKey = Persister::POLYMORPHIC_KEY;
444
        } else {
445
            return;
446
        }
447
448
        $converter = $this->getQueryRootConverter($metadata, $dbKey);
449 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...
450
            $value = (true === $this->hasOperators($value)) ? $value : ['$in' => $value];
451
            return [$dbKey, $this->formatQueryExpression($value, $converter)];
452
        }
453
        return [$dbKey, $converter($value)];
454
    }
455
456
    /**
457
     * Formats a query expression.
458
     *
459
     * @param   array   $expression
460
     * @param   Closure $converter
461
     * @return  array
462
     */
463
    private function formatQueryExpression(array $expression, Closure $converter)
464
    {
465
        foreach ($expression as $key => $value) {
466
467
            if ('$regex' === $key && !$value instanceof \MongoRegex) {
468
                $expression[$key] = new \MongoRegex($value);
469
                continue;
470
            }
471
472
            if (true === $this->isOpType('ignore', $key)) {
473
                continue;
474
            }
475
476
            if (true === $this->isOpType('single', $key)) {
477
                $expression[$key] = $converter($value);
478
                continue;
479
            }
480
481
            if (true === $this->isOpType('multiple', $key)) {
482
                $value = (array) $value;
483
                foreach ($value as $subKey => $subValue) {
484
                    $expression[$key][$subKey] = $converter($subValue);
485
                }
486
                continue;
487
            }
488
489
            if (true === $this->isOpType('recursive', $key)) {
490
                $value = (array) $value;
491
                $expression[$key] = $this->formatQueryExpression($value, $converter);
492
                continue;
493
            }
494
495
            $expression[$key] = $converter($value);
496
        }
497
        return $expression;
498
    }
499
500
    /**
501
     * Gets the converter for handling attribute values in queries.
502
     *
503
     * @param   Store               $store
504
     * @param   AttributeMetadata   $attrMeta
505
     * @return  Closure
506
     */
507
    private function getQueryAttrConverter(Store $store, AttributeMetadata $attrMeta)
508
    {
509
        return function ($value) use ($store, $attrMeta) {
510
            if (in_array($attrMeta->dataType, ['object', 'array'])) {
511
                // Leave the value as is.
512
                return $value;
513
            }
514
            $value = $store->convertAttributeValue($attrMeta->dataType, $value);
515
            return $this->getAttributeDbValue($attrMeta, $value);
516
        };
517
518
    }
519
520
    /**
521
     * Gets the converter for handling relationship values in queries.
522
     *
523
     * @param   Store                   $store
524
     * @param   RelationshipMetadata    $relMeta
525
     * @return  Closure
526
     */
527
    private function getQueryRelConverter(Store $store, RelationshipMetadata $relMeta)
528
    {
529
        $related = $store->getMetadataForType($relMeta->getEntityType());
530
        return $this->getQueryRootConverter($related, Persister::IDENTIFIER_KEY);
531
    }
532
533
    /**
534
     * Gets the converter for handling root field values in queries (id or model type).
535
     *
536
     * @param   EntityMetadata  $metadata
537
     * @param   string          $key
538
     * @return  Closure
539
     */
540
    private function getQueryRootConverter(EntityMetadata $metadata, $key)
541
    {
542
        return function($value) use ($metadata, $key) {
543
            if ($key === Persister::POLYMORPHIC_KEY) {
544
                return $value;
545
            }
546
            $strategy = ($metadata->persistence instanceof StorageMetadata) ? $metadata->persistence->idStrategy : null;
547
            return $this->getIdentifierDbValue($value, $strategy);
548
        };
549
    }
550
551
    /**
552
     * Determines whether a query value has additional query operators.
553
     *
554
     * @param   mixed   $value
555
     * @return  bool
556
     */
557
    private function hasOperators($value)
558
    {
559
560
        if (!is_array($value) && !is_object($value)) {
561
            return false;
562
        }
563
564
        if (is_object($value)) {
565
            $value = get_object_vars($value);
566
        }
567
568
        foreach ($value as $key => $subValue) {
569
            if (true === $this->isOperator($key)) {
570
                return true;
571
            }
572
        }
573
        return false;
574
    }
575
576
    /**
577
     * Determines if a key is a query operator.
578
     *
579
     * @param   string  $key
580
     * @return  bool
581
     */
582
    private function isOperator($key)
583
    {
584
        return isset($key[0]) && '$' === $key[0];
585
    }
586
587
    /**
588
     * Determines if a key is of a certain operator handling type.
589
     *
590
     * @param   string  $type
591
     * @param   string  $key
592
     * @return  bool
593
     */
594
    private function isOpType($type, $key)
595
    {
596
        if (!isset($this->ops[$type])) {
597
            return false;
598
        }
599
        return in_array($key, $this->ops[$type]);
600
    }
601
}
602