Completed
Pull Request — master (#5)
by Jacob
03:15
created

Formatter::getIdentifierDbValue()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 10
rs 9.4285
cc 3
eloc 6
nc 3
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\EntityMetadata;
9
use As3\Modlr\Metadata\RelationshipMetadata;
10
use As3\Modlr\Models\Model;
11
use As3\Modlr\Persister\PersisterException;
12
use As3\Modlr\Store\Store;
13
14
/**
15
 * Handles persistence formatting operations for MongoDB.
16
 * - Formats query criteria to proper keys and values.
17
 * - Formats attribute and relationships values for insertion to the db.
18
 * - Formats identifier values for insertion to the db.
19
 *
20
 * @author Jacob Bare <[email protected]>
21
 */
22
final class Formatter
23
{
24
    /**
25
     * Query operators.
26
     * Organized into handling groups.
27
     *
28
     * @var array
29
     */
30
    private $ops = [
31
        'root'      => ['$and', '$or', '$nor'],
32
        'single'    => ['$eq', '$gt', '$gte', '$lt', '$lte', '$ne'],
33
        'multiple'  => ['$in', '$nin', '$all'],
34
        'recursive' => ['$not', '$elemMatch'],
35
        'ignore'    => ['$exists', '$type', '$mod', '$size', '$regex', '$text', '$where'],
36
    ];
37
38
    /**
39
     * Formats a set of query criteria for a Model.
40
     * Ensures the id and type fields are properly applied.
41
     * Ensures that values are properly converted to their database equivalents: e.g dates, mongo ids, etc.
42
     *
43
     * @param   EntityMetadata  $metadata
44
     * @param   Store           $store
45
     * @param   array           $criteria
46
     * @return  array
47
     */
48
    public function formatQuery(EntityMetadata $metadata, Store $store, array $criteria)
49
    {
50
        $formatted = [];
51
        foreach ($criteria as $key => $value) {
52
53
            if ($this->isOpType('root', $key) && is_array($value)) {
54
                foreach ($value as $subKey => $subValue) {
55
                    $formatted[$key][$subKey] = $this->formatQuery($metadata, $store, $subValue);
56
                }
57
                continue;
58
            }
59
60
            if ($this->isOperator($key) && is_array($value)) {
61
                $formatted[$key] = $this->formatQuery($metadata, $store, $value);
62
                continue;
63
            }
64
65
            list($key, $value) = $this->formatQueryElement($key, $value, $metadata, $store);
66
            $formatted[$key] = $value;
67
        }
68
        return $formatted;
69
    }
70
71
    /**
72
     * Prepares and formats an attribute value for proper insertion into the database.
73
     *
74
     * @param   AttributeMetadata   $attrMeta
75
     * @param   mixed               $value
76
     * @return  mixed
77
     */
78 View Code Duplication
    public function getAttributeDbValue(AttributeMetadata $attrMeta, $value)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
79
    {
80
        // Handle data type conversion, if needed.
81
        if ('date' === $attrMeta->dataType && $value instanceof \DateTime) {
82
            return new \MongoDate($value->getTimestamp(), $value->format('u'));
83
        }
84
        return $value;
85
    }
86
87
    /**
88
     * Prepares and formats a has-one relationship model for proper insertion into the database.
89
     *
90
     * @param   RelationshipMetadata    $relMeta
91
     * @param   Model|null              $model
92
     * @return  mixed
93
     */
94 View Code Duplication
    public function getHasOneDbValue(RelationshipMetadata $relMeta, Model $model = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
95
    {
96
        if (null === $model || true === $relMeta->isInverse) {
97
            return null;
98
        }
99
        return $this->createReference($relMeta, $model);
100
    }
101
102
    /**
103
     * Prepares and formats a has-many relationship model set for proper insertion into the database.
104
     *
105
     * @param   RelationshipMetadata    $relMeta
106
     * @param   Model[]|null            $models
107
     * @return  mixed
108
     */
109 View Code Duplication
    public function getHasManyDbValue(RelationshipMetadata $relMeta, array $models = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
110
    {
111
        if (null === $models || true === $relMeta->isInverse) {
112
            return null;
113
        }
114
        $references = [];
115
        foreach ($models as $model) {
116
            $references[] = $this->createReference($relMeta, $model);
117
        }
118
        return empty($references) ? null : $references;
119
    }
120
121
    /**
122
     * {@inheritDoc}
123
     */
124
    public function getIdentifierDbValue($identifier, $strategy = null)
125
    {
126
        if (false === $this->isIdStrategySupported($strategy)) {
127
            throw PersisterException::nyi('ID conversion currently only supports an object strategy, or none at all.');
128
        }
129
        if ($identifier instanceof MongoId) {
130
            return $identifier;
131
        }
132
        return new MongoId($identifier);
133
    }
134
135
    /**
136
     * Gets all possible identifier field keys (internal and persistence layer).
137
     *
138
     * @return  array
139
     */
140
    public function getIdentifierFields()
141
    {
142
        return [Persister::IDENTIFIER_KEY, EntityMetadata::ID_KEY];
143
    }
144
145
    /**
146
     * Gets all possible model type keys (internal and persistence layer).
147
     *
148
     * @return  array
149
     */
150
    public function getTypeFields()
151
    {
152
        return [Persister::POLYMORPHIC_KEY, EntityMetadata::TYPE_KEY];
153
    }
154
155
    /**
156
     * Determines if a field key is an identifier.
157
     * Uses both the internal and persistence identifier keys.
158
     *
159
     * @param   string  $key
160
     * @return  bool
161
     */
162
    public function isIdentifierField($key)
163
    {
164
        return in_array($key, $this->getIdentifierFields());
165
    }
166
167
    /**
168
     * Determines if the provided id strategy is supported.
169
     *
170
     * @param   string|null     $strategy
171
     * @return  bool
172
     */
173
    public function isIdStrategySupported($strategy)
174
    {
175
        return (null === $strategy || 'object' === $strategy);
176
    }
177
178
    /**
179
     * Determines if a field key is a model type field.
180
     * Uses both the internal and persistence model type keys.
181
     *
182
     * @param   string  $key
183
     * @return  bool
184
     */
185
    public function isTypeField($key)
186
    {
187
        return in_array($key, $this->getTypeFields());
188
    }
189
190
    /**
191
     * Creates a reference for storage of a related model in the database
192
     *
193
     * @param   RelationshipMetadata    $relMeta
194
     * @param   Model                   $model
195
     * @return  mixed
196
     */
197
    private function createReference(RelationshipMetadata $relMeta, Model $model)
198
    {
199
        $identifier = $this->getIdentifierDbValue($model->getId());
200 View Code Duplication
        if (true === $relMeta->isPolymorphic()) {
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...
201
            $reference[$this->getIdentifierKey()] = $identifier;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$reference was never initialized. Although not strictly required by PHP, it is generally a good practice to add $reference = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
Bug introduced by
The method getIdentifierKey() does not seem to exist on object<As3\Modlr\Persister\MongoDb\Formatter>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
202
            $reference[$this->getPolymorphicKey()] = $model->getType();
0 ignored issues
show
Bug introduced by
The method getPolymorphicKey() does not seem to exist on object<As3\Modlr\Persister\MongoDb\Formatter>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
203
            return $reference;
204
        }
205
        return $identifier;
206
    }
207
208
    /**
209
     * Formats a query element and ensures the correct key and value are set.
210
     * Returns a tuple of the formatted key and value.
211
     *
212
     * @param   string          $key
213
     * @param   mixed           $value
214
     * @param   EntityMetadata  $metadata
215
     * @param   Store           $store
216
     * @return  array
217
     */
218
    private function formatQueryElement($key, $value, EntityMetadata $metadata, Store $store)
219
    {
220
        // Handle identifiers.
221
        if (null !== $result = $this->formatQueryElementId($key, $value, $metadata)) {
222
            return $result;
223
        }
224
225
        // Handle polymorphic model type fields.
226
        if (null !== $result = $this->formatQueryElementType($key, $value)) {
227
            return $result;
228
        }
229
230
        // Handle attributes.
231
        if (null !== $result = $this->formatQueryElementAttr($key, $value, $metadata, $store)) {
232
            return $result;
233
        }
234
235
        // Handle relationships.
236
        if (null !== $result = $this->formatQueryElementRel($key, $value, $metadata, $store)) {
237
            return $result;
238
        }
239
240
        // Handle dot notated fields.
241
        if (null !== $result = $this->formatQueryElementDotted($key, $value, $metadata, $store)) {
242
            return $result;
243
        }
244
245
        // Pass remaining elements unconverted.
246
        return [$key, $value];
247
    }
248
249
    /**
250
     * Formats an attribute query element.
251
     * Returns a tuple of the formatted key and value, or null if the key is not an attribute field.
252
     *
253
     * @param   string          $key
254
     * @param   mixed           $value
255
     * @param   EntityMetadata  $metadata
256
     * @param   Store           $store
257
     * @return  array|null
258
     */
259
    private function formatQueryElementAttr($key, $value, EntityMetadata $metadata, Store $store)
260
    {
261
        if (false === $metadata->hasAttribute($key)) {
262
            return;
263
        }
264
265
        $attrMeta = $metadata->getAttribute($key);
266
        $converter = $this->getQueryAttrConverter($store, $attrMeta);
0 ignored issues
show
Bug introduced by
It seems like $attrMeta defined by $metadata->getAttribute($key) on line 265 can be null; however, As3\Modlr\Persister\Mong...getQueryAttrConverter() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
267
268
        if (is_array($value)) {
269
270
            if (true === $this->hasOperators($value)) {
271
                return [$key, $this->formatQueryExpression($value, $converter)];
272
            }
273
274
            if (in_array($attrMeta->dataType, ['array', 'object'])) {
275
                return [$key, $value];
276
            }
277
            return [$key, $this->formatQueryExpression(['$in' => $value], $converter)];
278
        }
279
        return [$key, $converter($value)];
280
    }
281
282
    /**
283
     * Formats a dot-notated field.
284
     * Returns a tuple of the formatted key and value, or null if the key is not a dot-notated field, or cannot be handled.
285
     *
286
     * @param   string          $key
287
     * @param   mixed           $value
288
     * @param   EntityMetadata  $metadata
289
     * @param   Store           $store
290
     * @return  array|null
291
     */
292
    private function formatQueryElementDotted($key, $value, EntityMetadata $metadata, Store $store)
293
    {
294
        if (false === stripos($key, '.')) {
295
            return;
296
        }
297
298
        $parts = explode('.', $key);
299
        $root = array_shift($parts);
300
        if (false === $metadata->hasRelationship($root)) {
301
            // Nothing to format. Allow the dotted field to pass normally.
302
            return [$key, $value];
303
        }
304
        $hasIndex = is_numeric($parts[0]);
305
306
        if (true === $hasIndex) {
307
            $subKey = isset($parts[1]) ? $parts[1] : 'id';
308
        } else {
309
            $subKey = $parts[0];
310
        }
311
312 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...
313
            // Handle like a regular relationship
314
            list($key, $value) = $this->formatQueryElementRel($root, $value, $metadata, $store);
315
            $key = (true === $hasIndex) ? sprintf('%s.%s', $key, $parts[0]) : $key;
316
            return [$key, $value];
317
        }
318
319 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...
320
            // Handle as a model type field.
321
            list($key, $value) = $this->formatQueryElementType($subKey, $value);
322
            $key = (true === $hasIndex) ? sprintf('%s.%s.%s', $root, $parts[0], $key) : sprintf('%s.%s', $root, $key);
323
            return [$key, $value];
324
        }
325
        return [$key, $value];
326
    }
327
328
    /**
329
     * Formats an identifier query element.
330
     * Returns a tuple of the formatted key and value, or null if the key is not an identifier field.
331
     *
332
     * @param   string          $key
333
     * @param   mixed           $value
334
     * @param   EntityMetadata  $metadata
335
     * @return  array|null
336
     */
337 View Code Duplication
    private function formatQueryElementId($key, $value, EntityMetadata $metadata)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
338
    {
339
        if (false === $this->isIdentifierField($key)) {
340
            return;
341
        }
342
343
        $dbIdKey = Persister::IDENTIFIER_KEY;
344
        $converter = $this->getQueryIdConverter($metadata);
345
346
        if (is_array($value)) {
347
            $value = (true === $this->hasOperators($value)) ? $value : ['$in' => $value];
348
            return [$dbIdKey, $this->formatQueryExpression($value, $converter)];
349
        }
350
        return [$dbIdKey, $converter($value)];
351
    }
352
353
    /**
354
     * Formats a relationship query element.
355
     * Returns a tuple of the formatted key and value, or null if the key is not a relationship field.
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 formatQueryElementRel($key, $value, EntityMetadata $metadata, Store $store)
364
    {
365
        if (false === $metadata->hasRelationship($key)) {
366
            return;
367
        }
368
369
        $relMeta = $metadata->getRelationship($key);
370
        $converter = $this->getQueryRelConverter($store, $relMeta);
0 ignored issues
show
Bug introduced by
It seems like $relMeta defined by $metadata->getRelationship($key) on line 369 can be null; however, As3\Modlr\Persister\Mong...:getQueryRelConverter() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
371
372
        if (true === $relMeta->isPolymorphic()) {
373
            $key = sprintf('%s.%s', $key, Persister::IDENTIFIER_KEY);
374
        }
375
376
        if (is_array($value)) {
377
            $value = (true === $this->hasOperators($value)) ? $value : ['$in' => $value];
378
            return [$key, $this->formatQueryExpression($value, $converter)];
379
        }
380
        return [$key, $converter($value)];
381
    }
382
383
    /**
384
     * Formats a model type query element.
385
     * Returns a tuple of the formatted key and value, or null if the key is not a type field.
386
     *
387
     * @param   string  $key
388
     * @param   mixed   $value
389
     * @return  array|null
390
     */
391 View Code Duplication
    private function formatQueryElementType($key, $value)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
392
    {
393
        if (false === $this->isTypeField($key)) {
394
            return;
395
        }
396
397
        $dbTypeKey = Persister::POLYMORPHIC_KEY;
398
        $converter = $this->getQueryTypeConverter();
399
400
        if (is_array($value)) {
401
            $value = (true === $this->hasOperators($value)) ? $value : ['$in' => $value];
402
            return [$dbTypeKey, $this->formatQueryExpression($value, $converter)];
403
        }
404
        return [$dbTypeKey, $converter($value)];
405
    }
406
407
    /**
408
     * Formats a query expression.
409
     *
410
     * @param   array   $expression
411
     * @param   Closure $converter
412
     * @return  array
413
     */
414
    private function formatQueryExpression(array $expression, Closure $converter)
415
    {
416
        foreach ($expression as $key => $value) {
417
418
            if (true === $this->isOpType('ignore', $key)) {
419
                continue;
420
            }
421
422
            if (true === $this->isOpType('single', $key)) {
423
                $expression[$key] = $converter($value);
424
                continue;
425
            }
426
427
            if (true === $this->isOpType('multiple', $key)) {
428
                $value = (array) $value;
429
                foreach ($value as $subKey => $subValue) {
430
                    $expression[$key][$subKey] = $converter($subValue);
431
                }
432
                continue;
433
            }
434
435
            if (true === $this->isOpType('recursive', $key)) {
436
                $value = (array) $value;
437
                $expression[$key] = $this->formatQueryExpression($value, $converter);
438
                continue;
439
            }
440
441
            $expression[$key] = $converter($value);
442
        }
443
        return $expression;
444
    }
445
446
    /**
447
     * Gets the converter for handling attribute values in queries.
448
     *
449
     * @param   Store               $store
450
     * @param   AttributeMetadata   $attrMeta
451
     * @return  Closure
452
     */
453
    private function getQueryAttrConverter(Store $store, AttributeMetadata $attrMeta)
454
    {
455
        return function ($value) use ($store, $attrMeta) {
456
            $value = $store->convertAttributeValue($attrMeta->dataType, $value);
457
            return $this->getAttributeDbValue($attrMeta, $value);
458
        };
459
460
    }
461
462
    /**
463
     * Gets the converter for handling identifier values in queries.
464
     *
465
     * @param   EntityMetadata  $metadata
466
     * @return  Closure
467
     */
468
    private function getQueryIdConverter(EntityMetadata $metadata)
469
    {
470
        return function($value) use ($metadata) {
471
            return $this->getIdentifierDbValue($value, $metadata->persistence->idStrategy);
0 ignored issues
show
Bug introduced by
Accessing idStrategy 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...
472
        };
473
    }
474
475
    /**
476
     * Gets the converter for handling relationship values in queries.
477
     *
478
     * @param   Store                   $store
479
     * @param   RelationshipMetadata    $relMeta
480
     * @return  Closure
481
     */
482
    private function getQueryRelConverter(Store $store, RelationshipMetadata $relMeta)
483
    {
484
        $related = $store->getMetadataForType($relMeta->getEntityType());
485
        return $this->getQueryIdConverter($related);
486
    }
487
488
    /**
489
     * Gets the converter for handling model type values in queries.
490
     *
491
     * @return  Closure
492
     */
493
    private function getQueryTypeConverter()
494
    {
495
        return function($value) {
496
            return $value;
497
        };
498
    }
499
500
    /**
501
     * Determines whether a query value has additional query operators.
502
     *
503
     * @param   mixed   $value
504
     * @return  bool
505
     */
506
    private function hasOperators($value)
507
    {
508
509
        if (!is_array($value) && !is_object($value)) {
510
            return false;
511
        }
512
513
        if (is_object($value)) {
514
            $value = get_object_vars($value);
515
        }
516
517
        foreach ($value as $key => $subValue) {
518
            if (true === $this->isOperator($key)) {
519
                return true;
520
            }
521
        }
522
        return false;
523
    }
524
525
    /**
526
     * Determines if a key is a query operator.
527
     *
528
     * @param   string  $key
529
     * @return  bool
530
     */
531
    private function isOperator($key)
532
    {
533
        return isset($key[0]) && '$' === $key[0];
534
    }
535
536
    /**
537
     * Determines if a key is of a certain operator handling type.
538
     *
539
     * @param   string  $type
540
     * @param   string  $key
541
     * @return  bool
542
     */
543
    private function isOpType($type, $key)
544
    {
545
        if (!isset($this->ops[$type])) {
546
            return false;
547
        }
548
        return in_array($key, $this->ops[$type]);
549
    }
550
}
551