EntityDefinitionQueryHelper::getTranslatedField()   A
last analyzed

Complexity

Conditions 5
Paths 3

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 11
nc 3
nop 2
dl 0
loc 21
rs 9.6111
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Shopware\Core\Framework\DataAbstractionLayer\Dbal;
4
5
use Doctrine\DBAL\ArrayParameterType;
6
use Doctrine\DBAL\Connection;
7
use Shopware\Core\Defaults;
8
use Shopware\Core\Framework\Context;
9
use Shopware\Core\Framework\DataAbstractionLayer\Dbal\Exception\FieldAccessorBuilderNotFoundException;
10
use Shopware\Core\Framework\DataAbstractionLayer\Dbal\Exception\UnmappedFieldException;
11
use Shopware\Core\Framework\DataAbstractionLayer\Dbal\FieldResolver\FieldResolverContext;
12
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
13
use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;
14
use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
15
use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
16
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Inherited;
17
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\PrimaryKey;
18
use Shopware\Core\Framework\DataAbstractionLayer\Field\IdField;
19
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;
20
use Shopware\Core\Framework\DataAbstractionLayer\Field\ReferenceVersionField;
21
use Shopware\Core\Framework\DataAbstractionLayer\Field\StorageAware;
22
use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslatedField;
23
use Shopware\Core\Framework\DataAbstractionLayer\Field\VersionField;
24
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
25
use Shopware\Core\Framework\DataAbstractionLayer\Search\CriteriaPartInterface;
26
use Shopware\Core\Framework\Log\Package;
27
use Shopware\Core\Framework\Uuid\Uuid;
28
29
/**
30
 * This class acts only as helper/common class for all dbal operations for entity definitions.
31
 * It knows how an association should be joined, how a parent-child inheritance should act, how translation chains work, ...
32
 *
33
 * @internal
34
 */
35
#[Package('core')]
36
class EntityDefinitionQueryHelper
37
{
38
    final public const HAS_TO_MANY_JOIN = 'has_to_many_join';
39
40
    public static function escape(string $string): string
41
    {
42
        if (mb_strpos($string, '`') !== false) {
43
            throw new \InvalidArgumentException('Backtick not allowed in identifier');
44
        }
45
46
        return '`' . $string . '`';
47
    }
48
49
    public static function columnExists(Connection $connection, string $table, string $column): bool
50
    {
51
        $exists = $connection->fetchOne(
52
            'SHOW COLUMNS FROM ' . self::escape($table) . ' WHERE `Field` LIKE :column',
53
            ['column' => $column]
54
        );
55
56
        return !empty($exists);
57
    }
58
59
    public static function tableExists(Connection $connection, string $table): bool
60
    {
61
        return !empty(
62
            $connection->fetchOne(
63
                'SHOW TABLES LIKE :table',
64
                [
65
                    'table' => $table,
66
                ]
67
            )
68
        );
69
    }
70
71
    /**
72
     * @return list<Field>
73
     */
74
    public static function getFieldsOfAccessor(EntityDefinition $definition, string $accessor, bool $resolveTranslated = true): array
75
    {
76
        $parts = explode('.', $accessor);
77
        if ($definition->getEntityName() === $parts[0]) {
78
            array_shift($parts);
79
        }
80
81
        $accessorFields = [];
82
83
        $source = $definition;
84
85
        foreach ($parts as $part) {
86
            if ($part === 'extensions') {
87
                continue;
88
            }
89
90
            $fields = $source->getFields();
91
            $field = $fields->get($part);
92
93
            // continue if the current part is not a real field to allow access on collections
94
            if (!$field) {
95
                continue;
96
            }
97
98
            if ($field instanceof TranslatedField && $resolveTranslated) {
99
                /** @var EntityDefinition $source */
100
                $source = $source->getTranslationDefinition();
101
                $fields = $source->getFields();
102
                $accessorFields[] = $fields->get($part);
103
104
                continue;
105
            }
106
107
            if ($field instanceof TranslatedField && !$resolveTranslated) {
108
                $accessorFields[] = $field;
109
110
                continue;
111
            }
112
113
            $accessorFields[] = $field;
114
115
            if (!$field instanceof AssociationField) {
116
                break;
117
            }
118
119
            $source = $field->getReferenceDefinition();
120
            if ($field instanceof ManyToManyAssociationField) {
121
                $source = $field->getToManyReferenceDefinition();
122
            }
123
        }
124
125
        return \array_values(\array_filter($accessorFields));
126
    }
127
128
    /**
129
     * Returns the field instance of the provided fieldName.
130
     *
131
     * @example
132
     *
133
     * fieldName => 'product.name'
134
     * Returns the (new TranslatedField('name')) declaration
135
     *
136
     * Allows additionally nested referencing
137
     *
138
     * fieldName => 'category.products.name'
139
     * Returns as well the above field definition
140
     */
141
    public function getField(string $fieldName, EntityDefinition $definition, string $root, bool $resolveTranslated = true): ?Field
142
    {
143
        $original = $fieldName;
144
        $prefix = $root . '.';
145
146
        if (mb_strpos($fieldName, $prefix) === 0) {
147
            $fieldName = mb_substr($fieldName, mb_strlen($prefix));
148
        } else {
149
            $original = $prefix . $original;
150
        }
151
152
        $fields = $definition->getFields();
153
154
        $isAssociation = mb_strpos($fieldName, '.') !== false;
155
156
        if (!$isAssociation && $fields->has($fieldName)) {
157
            return $fields->get($fieldName);
158
        }
159
        $associationKey = explode('.', $fieldName);
160
        $associationKey = array_shift($associationKey);
161
162
        $field = $fields->get($associationKey);
163
164
        if ($field instanceof TranslatedField && $resolveTranslated) {
165
            return self::getTranslatedField($definition, $field);
166
        }
167
        if ($field instanceof TranslatedField) {
168
            return $field;
169
        }
170
171
        if (!$field instanceof AssociationField) {
172
            return $field;
173
        }
174
175
        $referenceDefinition = $field->getReferenceDefinition();
176
        if ($field instanceof ManyToManyAssociationField) {
177
            $referenceDefinition = $field->getToManyReferenceDefinition();
178
        }
179
180
        return $this->getField(
181
            $original,
182
            $referenceDefinition,
183
            $root . '.' . $field->getPropertyName()
184
        );
185
    }
186
187
    /**
188
     * Builds the sql field accessor for the provided field.
189
     *
190
     * @example
191
     *
192
     * fieldName => product.taxId
193
     * root      => product
194
     * returns   => `product`.`tax_id`
195
     *
196
     * This function is also used for complex field accessors like JsonArray Field, JsonObject fields.
197
     * It considers the translation and parent-child inheritance.
198
     *
199
     * fieldName => product.name
200
     * root      => product
201
     * return    => COALESCE(`product.translation`.`name`,`product.parent.translation`.`name`)
202
     *
203
     * @throws UnmappedFieldException
204
     */
205
    public function getFieldAccessor(string $fieldName, EntityDefinition $definition, string $root, Context $context): string
206
    {
207
        $fieldName = str_replace('extensions.', '', $fieldName);
208
209
        $original = $fieldName;
210
        $prefix = $root . '.';
211
212
        if (str_starts_with($fieldName, $prefix)) {
213
            $fieldName = mb_substr($fieldName, mb_strlen($prefix));
214
        } else {
215
            $original = $prefix . $original;
216
        }
217
218
        $fields = $definition->getFields();
219
        if ($fields->has($fieldName)) {
220
            $field = $fields->get($fieldName);
221
222
            return $this->buildInheritedAccessor($field, $root, $definition, $context, $fieldName);
223
        }
224
225
        $parts = explode('.', $fieldName);
226
        $associationKey = array_shift($parts);
227
228
        if ($associationKey === 'extensions') {
229
            $associationKey = array_shift($parts);
230
        }
231
232
        if (!\is_string($associationKey) || !$fields->has($associationKey)) {
233
            throw new UnmappedFieldException($original, $definition);
234
        }
235
236
        $field = $fields->get($associationKey);
237
238
        // case for json object fields, other fields has now same option to act with more point notations but hasn't to be an association field. E.g. price.gross
239
        if (!$field instanceof AssociationField && ($field instanceof StorageAware || $field instanceof TranslatedField)) {
240
            return $this->buildInheritedAccessor($field, $root, $definition, $context, $fieldName);
241
        }
242
243
        if (!$field instanceof AssociationField) {
244
            throw new \RuntimeException(sprintf('Expected field "%s" to be instance of %s', $associationKey, AssociationField::class));
245
        }
246
247
        $referenceDefinition = $field->getReferenceDefinition();
248
        if ($field instanceof ManyToManyAssociationField) {
249
            $referenceDefinition = $field->getToManyReferenceDefinition();
250
        }
251
252
        return $this->getFieldAccessor(
253
            $original,
254
            $referenceDefinition,
255
            $root . '.' . $field->getPropertyName(),
256
            $context
257
        );
258
    }
259
260
    public static function getAssociationPath(string $accessor, EntityDefinition $definition): ?string
261
    {
262
        $fields = self::getFieldsOfAccessor($definition, $accessor);
263
264
        $path = [];
265
        foreach ($fields as $field) {
266
            if (!$field instanceof AssociationField) {
267
                break;
268
            }
269
            $path[] = $field->getPropertyName();
270
        }
271
272
        if (empty($path)) {
273
            return null;
274
        }
275
276
        return implode('.', $path);
277
    }
278
279
    /**
280
     * Creates the basic root query for the provided entity definition and application context.
281
     * It considers the current context version.
282
     */
283
    public function getBaseQuery(QueryBuilder $query, EntityDefinition $definition, Context $context): QueryBuilder
284
    {
285
        $table = $definition->getEntityName();
286
287
        $query->from(self::escape($table));
288
289
        $useVersionFallback // only applies for versioned entities
290
            = $definition->isVersionAware()
291
            // only add live fallback if the current version isn't the live version
292
            && $context->getVersionId() !== Defaults::LIVE_VERSION
293
            // sub entities have no live fallback
294
            && $definition->getParentDefinition() === null;
295
296
        if ($useVersionFallback) {
297
            $this->joinVersion($query, $definition, $definition->getEntityName(), $context);
298
        } elseif ($definition->isVersionAware()) {
299
            $versionIdField = array_filter(
300
                $definition->getPrimaryKeys()->getElements(),
301
                fn ($f) => $f instanceof VersionField || $f instanceof ReferenceVersionField
302
            );
303
304
            if (!$versionIdField) {
305
                throw new \RuntimeException('Missing `VersionField` in `' . $definition->getClass() . '`');
306
            }
307
308
            /** @var FkField $versionIdField */
309
            $versionIdField = array_shift($versionIdField);
310
311
            $query->andWhere(self::escape($table) . '.' . self::escape($versionIdField->getStorageName()) . ' = :version');
312
            $query->setParameter('version', Uuid::fromHexToBytes($context->getVersionId()));
313
        }
314
315
        return $query;
316
    }
317
318
    /**
319
     * Used for dynamic sql joins. In case that the given fieldName is unknown or event nested with multiple association
320
     * roots, the function can resolve each association part of the field name, even if one part of the fieldName contains a translation or event inherited data field.
321
     */
322
    public function resolveAccessor(
323
        string $accessor,
324
        EntityDefinition $definition,
325
        string $root,
326
        QueryBuilder $query,
327
        Context $context,
328
        ?CriteriaPartInterface $criteriaPart = null
329
    ): void {
330
        $accessor = str_replace('extensions.', '', $accessor);
331
332
        $parts = explode('.', $accessor);
333
334
        if ($parts[0] === $root) {
335
            unset($parts[0]);
336
        }
337
338
        $alias = $root;
339
340
        $path = [$root];
341
342
        $rootDefinition = $definition;
343
344
        foreach ($parts as $part) {
345
            $field = $definition->getFields()->get($part);
346
347
            if ($field === null) {
348
                return;
349
            }
350
351
            $resolver = $field->getResolver();
352
            if ($resolver === null) {
353
                continue;
354
            }
355
356
            if ($field instanceof AssociationField) {
357
                $path[] = $field->getPropertyName();
358
            }
359
360
            $currentPath = implode('.', $path);
361
            $resolverContext = new FieldResolverContext($currentPath, $alias, $field, $definition, $rootDefinition, $query, $context, $criteriaPart);
362
363
            $alias = $this->callResolver($resolverContext);
364
365
            if (!$field instanceof AssociationField) {
366
                return;
367
            }
368
369
            $definition = $field->getReferenceDefinition();
370
            if ($field instanceof ManyToManyAssociationField) {
371
                $definition = $field->getToManyReferenceDefinition();
372
            }
373
374
            if ($definition->isInheritanceAware() && $context->considerInheritance() && $parent = $definition->getField('parent')) {
375
                $resolverContext = new FieldResolverContext($currentPath, $alias, $parent, $definition, $rootDefinition, $query, $context, $criteriaPart);
376
377
                $this->callResolver($resolverContext);
378
            }
379
        }
380
    }
381
382
    public function resolveField(Field $field, EntityDefinition $definition, string $root, QueryBuilder $query, Context $context): void
383
    {
384
        $resolver = $field->getResolver();
385
386
        if ($resolver === null) {
387
            return;
388
        }
389
390
        $resolver->join(new FieldResolverContext($root, $root, $field, $definition, $definition, $query, $context, null));
391
    }
392
393
    /**
394
     * Adds the full translation select part to the provided sql query.
395
     * Considers the parent-child inheritance and provided context language inheritance.
396
     * The raw parameter allows to skip the parent-child inheritance.
397
     *
398
     * @param array<string, mixed> $partial
399
     */
400
    public function addTranslationSelect(string $root, EntityDefinition $definition, QueryBuilder $query, Context $context, array $partial = []): void
401
    {
402
        $translationDefinition = $definition->getTranslationDefinition();
403
404
        if (!$translationDefinition) {
405
            return;
406
        }
407
408
        $fields = $translationDefinition->getFields();
409
        if (!empty($partial)) {
410
            $fields = $translationDefinition->getFields()->filter(fn (Field $field) => $field->is(PrimaryKey::class)
411
                || isset($partial[$field->getPropertyName()])
412
                || $field instanceof FkField);
413
        }
414
415
        $inherited = $context->considerInheritance() && $definition->isInheritanceAware();
416
417
        $chain = EntityDefinitionQueryHelper::buildTranslationChain($root, $context, $inherited);
418
419
        /** @var TranslatedField $field */
420
        foreach ($fields as $field) {
421
            if (!$field instanceof StorageAware) {
422
                continue;
423
            }
424
425
            $selects = [];
426
            foreach ($chain as $select) {
427
                $vars = [
428
                    '#root#' => $select,
429
                    '#field#' => $field->getPropertyName(),
430
                ];
431
432
                $query->addSelect(str_replace(
433
                    array_keys($vars),
434
                    array_values($vars),
435
                    EntityDefinitionQueryHelper::escape('#root#.#field#')
436
                ));
437
438
                $selects[] = str_replace(
439
                    array_keys($vars),
440
                    array_values($vars),
441
                    self::escape('#root#.#field#')
442
                );
443
            }
444
445
            // check if current field is a translated field of the origin definition
446
            $origin = $definition->getFields()->get($field->getPropertyName());
447
            if (!$origin instanceof TranslatedField) {
448
                continue;
449
            }
450
451
            $selects[] = self::escape($root . '.translation.' . $field->getPropertyName());
452
453
            // add selection for resolved parent-child and language inheritance
454
            $query->addSelect(
455
                sprintf('COALESCE(%s)', implode(',', $selects)) . ' as '
456
                . self::escape($root . '.' . $field->getPropertyName())
457
            );
458
        }
459
    }
460
461
    public function joinVersion(QueryBuilder $query, EntityDefinition $definition, string $root, Context $context): void
462
    {
463
        $table = $definition->getEntityName();
464
        $versionRoot = $root . '_version';
465
466
        $query->andWhere(
467
            str_replace(
468
                ['#root#', '#table#', '#version#'],
469
                [self::escape($root), self::escape($table), self::escape($versionRoot)],
470
                '#root#.version_id = COALESCE(
471
                    (SELECT DISTINCT version_id FROM #table# AS #version# WHERE #version#.`id` = #root#.`id` AND `version_id` = :version),
472
                    :liveVersion
473
                )'
474
            )
475
        );
476
477
        $query->setParameter('liveVersion', Uuid::fromHexToBytes(Defaults::LIVE_VERSION));
478
        $query->setParameter('version', Uuid::fromHexToBytes($context->getVersionId()));
479
    }
480
481
    public static function getTranslatedField(EntityDefinition $definition, TranslatedField $translatedField): Field
482
    {
483
        $translationDefinition = $definition->getTranslationDefinition();
484
485
        if ($translationDefinition === null) {
486
            throw new \RuntimeException(sprintf('Entity %s has no translation definition', $definition->getEntityName()));
487
        }
488
489
        $field = $translationDefinition->getFields()->get($translatedField->getPropertyName());
490
491
        if ($field === null || !$field instanceof StorageAware || !$field instanceof Field) {
492
            throw new \RuntimeException(
493
                sprintf(
494
                    'Missing translated storage aware property %s in %s',
495
                    $translatedField->getPropertyName(),
496
                    $translationDefinition->getEntityName()
497
                )
498
            );
499
        }
500
501
        return $field;
502
    }
503
504
    /**
505
     * @return list<string>
506
     */
507
    public static function buildTranslationChain(string $root, Context $context, bool $includeParent): array
508
    {
509
        $chain = [];
510
        $count = \count($context->getLanguageIdChain()) - 1;
511
512
        for ($i = $count; $i >= 1; --$i) {
513
            $chain[] = $root . '.translation.fallback_' . $i;
514
            if ($includeParent) {
515
                $chain[] = $root . '.parent.translation.fallback_' . $i;
516
            }
517
        }
518
519
        $chain[] = $root . '.translation';
520
        if ($includeParent) {
521
            $chain[] = $root . '.parent.translation';
522
        }
523
524
        return $chain;
525
    }
526
527
    public function addIdCondition(Criteria $criteria, EntityDefinition $definition, QueryBuilder $query): void
528
    {
529
        $primaryKeys = $criteria->getIds();
530
531
        $primaryKeys = array_values($primaryKeys);
532
533
        if (empty($primaryKeys)) {
534
            return;
535
        }
536
537
        if (!\is_array($primaryKeys[0]) || \count($primaryKeys[0]) === 1) {
538
            $primaryKeyField = $definition->getPrimaryKeys()->first();
539
            if ($primaryKeyField instanceof IdField || $primaryKeyField instanceof FkField) {
540
                $primaryKeys = array_map(function ($id) {
541
                    if (\is_array($id)) {
542
                        /** @var string $shiftedId */
543
                        $shiftedId = array_shift($id);
544
545
                        return Uuid::fromHexToBytes($shiftedId);
546
                    }
547
548
                    return Uuid::fromHexToBytes($id);
549
                }, $primaryKeys);
550
            }
551
552
            if (!$primaryKeyField instanceof StorageAware) {
553
                throw new \RuntimeException('Primary key fields has to be an instance of StorageAware');
554
            }
555
556
            $query->andWhere(sprintf(
557
                '%s.%s IN (:ids)',
558
                EntityDefinitionQueryHelper::escape($definition->getEntityName()),
559
                EntityDefinitionQueryHelper::escape($primaryKeyField->getStorageName())
560
            ));
561
562
            $query->setParameter('ids', $primaryKeys, ArrayParameterType::STRING);
563
564
            return;
565
        }
566
567
        $this->addIdConditionWithOr($criteria, $definition, $query);
568
    }
569
570
    private function callResolver(FieldResolverContext $context): string
571
    {
572
        $resolver = $context->getField()->getResolver();
573
574
        if (!$resolver) {
575
            return $context->getAlias();
576
        }
577
578
        return $resolver->join($context);
579
    }
580
581
    private function addIdConditionWithOr(Criteria $criteria, EntityDefinition $definition, QueryBuilder $query): void
582
    {
583
        $wheres = [];
584
585
        foreach ($criteria->getIds() as $primaryKey) {
586
            if (!\is_array($primaryKey)) {
587
                $primaryKey = ['id' => $primaryKey];
588
            }
589
590
            $where = [];
591
592
            foreach ($primaryKey as $propertyName => $value) {
593
                $field = $definition->getFields()->get($propertyName);
594
595
                if (!$field) {
596
                    throw new UnmappedFieldException($propertyName, $definition);
597
                }
598
599
                if (!$field instanceof StorageAware) {
600
                    throw new \RuntimeException('Only storage aware fields are supported in read condition');
601
                }
602
603
                if ($field instanceof IdField || $field instanceof FkField) {
604
                    $value = Uuid::fromHexToBytes($value);
605
                }
606
607
                $key = 'pk' . Uuid::randomHex();
608
609
                $accessor = EntityDefinitionQueryHelper::escape($definition->getEntityName()) . '.' . EntityDefinitionQueryHelper::escape($field->getStorageName());
610
611
                $where[$accessor] = $accessor . ' = :' . $key;
612
613
                $query->setParameter($key, $value);
614
            }
615
616
            $wheres[] = '(' . implode(' AND ', $where) . ')';
617
        }
618
619
        $wheres = implode(' OR ', $wheres);
620
621
        $query->andWhere($wheres);
622
    }
623
624
    /**
625
     * @param list<string> $chain
626
     */
627
    private function getTranslationFieldAccessor(Field $field, string $accessor, array $chain, Context $context): string
628
    {
629
        if (!$field instanceof StorageAware) {
630
            throw new \RuntimeException('Only storage aware fields are supported as translated field');
631
        }
632
633
        $selects = [];
634
        foreach ($chain as $part) {
635
            $select = $this->buildFieldSelector($part, $field, $context, $accessor);
636
637
            $selects[] = str_replace(
638
                '`.' . self::escape($field->getStorageName()),
639
                '.' . $field->getPropertyName() . '`',
640
                $select
641
            );
642
        }
643
644
        /*
645
         * Simplified Example:
646
         * COALESCE(
647
             JSON_UNQUOTE(JSON_EXTRACT(`tbl.translation.fallback_2`.`translated_attributes`, '$.path')) AS datetime(3), # child language
648
             JSON_UNQUOTE(JSON_EXTRACT(`tbl.translation.fallback_1`.`translated_attributes`, '$.path')) AS datetime(3), # root language
649
             JSON_UNQUOTE(JSON_EXTRACT(`tbl.translation`.`translated_attributes`, '$.path')) AS datetime(3) # system language
650
           );
651
         */
652
        return sprintf('COALESCE(%s)', implode(',', $selects));
653
    }
654
655
    private function buildInheritedAccessor(
656
        Field $field,
657
        string $root,
658
        EntityDefinition $definition,
659
        Context $context,
660
        string $original
661
    ): string {
662
        if ($field instanceof TranslatedField) {
663
            $inheritedChain = self::buildTranslationChain($root, $context, $definition->isInheritanceAware() && $context->considerInheritance());
664
665
            $translatedField = self::getTranslatedField($definition, $field);
666
667
            return $this->getTranslationFieldAccessor($translatedField, $original, $inheritedChain, $context);
668
        }
669
670
        $select = $this->buildFieldSelector($root, $field, $context, $original);
671
672
        if (!$field->is(Inherited::class) || !$context->considerInheritance()) {
673
            return $select;
674
        }
675
676
        $parentSelect = $this->buildFieldSelector($root . '.parent', $field, $context, $original);
677
678
        return sprintf('IFNULL(%s, %s)', $select, $parentSelect);
679
    }
680
681
    private function buildFieldSelector(string $root, Field $field, Context $context, string $accessor): string
682
    {
683
        $accessorBuilder = $field->getAccessorBuilder();
684
        if (!$accessorBuilder) {
685
            throw new FieldAccessorBuilderNotFoundException($field->getPropertyName());
686
        }
687
688
        $accessor = $accessorBuilder->buildAccessor($root, $field, $context, $accessor);
689
690
        if (!$accessor) {
691
            throw new \RuntimeException(sprintf('Can not build accessor for field "%s" on root "%s"', $field->getPropertyName(), $root));
692
        }
693
694
        return $accessor;
695
    }
696
}
697