Completed
Branch develop (85a9c8)
by Anton
05:44
created

Loader::clarifySelector()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 1
nc 1
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
namespace Spiral\ORM\Entities;
9
10
use Spiral\Database\Entities\Database;
11
use Spiral\Database\Query\QueryResult;
12
use Spiral\ORM\Entities\Loaders\RootLoader;
13
use Spiral\ORM\Exceptions\LoaderException;
14
use Spiral\ORM\LoaderInterface;
15
use Spiral\ORM\ORM;
16
use Spiral\ORM\RecordEntity;
17
18
/**
19
 * ORM Loaders used to load an compile data tree based on results fetched from SQL databases,
20
 * loaders can communicate with parent selector by providing it's own set of conditions, columns
21
 * joins and etc. In some cases loader may create additional selector to load data using information
22
 * fetched from previous query. Every loaded must be associated with specific record schema and
23
 * relation (except RootLoader).
24
 *
25
 * Loaders can be used for both - loading and filtering of record data.
26
 *
27
 * Reference tree generation logic example:
28
 * User has many Posts (relation "posts"), user primary is ID, post inner key pointing to user
29
 * is USER_ID. Post loader must request User data loader to create references based on ID field
30
 * values. Once Post data were parsed we can mount it under parent user using mount method:
31
 *
32
 * $this->parent->mount("posts", "ID", $data["USER_ID"], $data, true); //true = multiple
33
 *
34
 * @see Selector::load()
35
 * @see Selector::with()
36
 */
37
abstract class Loader implements LoaderInterface
38
{
39
    /**
40
     * Default loading methods for ORM loaders.
41
     */
42
    const INLOAD   = 1;
43
    const POSTLOAD = 2;
44
    const JOIN     = 3;
45
46
    /**
47
     * Relation type is required to correctly resolve foreign record class based on relation
48
     * definition.
49
     */
50
    const RELATION_TYPE = null;
51
52
    /**
53
     * Default load method (inload or postload).
54
     */
55
    const LOAD_METHOD = null;
56
57
    /**
58
     * Internal loader constant used to decide how to aggregate data tree, true for relations like
59
     * MANY TO MANY or HAS MANY.
60
     */
61
    const MULTIPLE = false;
62
63
    /**
64
     * Count of Loaders requested data alias.
65
     *
66
     * @var int
67
     */
68
    private static $counter = 0;
69
70
    /**
71
     * Unique loader data alias (only for loaders, not joiners).
72
     *
73
     * @var string
74
     */
75
    private $alias = '';
76
77
    /**
78
     * Helper structure used to prevent data duplication when LEFT JOIN multiplies parent records.
79
     *
80
     * @invisible
81
     * @var array
82
     */
83
    private $duplicates = [];
84
85
    /**
86
     * Loader configuration options, can be edited using setOptions method or while declaring loader
87
     * in Selector.
88
     *
89
     * @var array
90
     */
91
    protected $options = [
92
        'method' => null,
93
        'alias'  => null,
94
        'using'  => null,
95
        'where'  => null
96
    ];
97
98
    /**
99
     * Result of data compilation, only populated in cases where loader is primary Selector loader.
100
     *
101
     * @var array
102
     */
103
    protected $result = [];
104
105
    /**
106
     * Container related to parent loader. Loaded data must be loaded using this container.
107
     *
108
     * @var string
109
     */
110
    protected $container = '';
111
112
    /**
113
     * Indication that loaded already set columns and conditions to parent Selector.
114
     *
115
     * @var bool
116
     */
117
    protected $configured = false;
118
119
    /**
120
     * Set of columns to be fetched from resulted query.
121
     *
122
     * @var array
123
     */
124
    protected $dataColumns = [];
125
126
    /**
127
     * Loader data offset in resulted query row provided by parent Selector or Loader.
128
     *
129
     * @var int
130
     */
131
    protected $dataOffset = 0;
132
133
    /**
134
     * Relation definition options if any.
135
     *
136
     * @var array
137
     */
138
    protected $definition = [];
139
140
    /**
141
     * Inner (nested) loaders.
142
     *
143
     * @var LoaderInterface[]
144
     */
145
    protected $loaders = [];
146
147
    /**
148
     * Loaders used purely for conditional purposes. Only ORM loaders can do that.
149
     *
150
     * @var Loader[]
151
     */
152
    protected $joiners = [];
153
154
    /**
155
     * Set of keys requested by inner loaders to be pre-aggregated while query parsing. This
156
     * structure if populated when new sub loaded registered.
157
     *
158
     * @var array
159
     */
160
    protected $referenceKeys = [];
161
162
    /**
163
     * Chunks of parsed data associated with their reference key name and it's value. Used to
164
     * compile data tree via php references.
165
     *
166
     * @var array
167
     */
168
    protected $references = [];
169
170
    /**
171
     * Related record schema.
172
     *
173
     * @invisible
174
     * @var array
175
     */
176
    protected $schema = [];
177
178
    /**
179
     * ORM Loaders can only be nested into ORM Loaders.
180
     *
181
     * @invisible
182
     * @var Loader|null
183
     */
184
    protected $parent = null;
185
186
    /**
187
     * @invisible
188
     * @var ORM
189
     */
190
    protected $orm = null;
191
192
    /**
193
     * {@inheritdoc}
194
     */
195
    public function __construct(
196
        ORM $orm,
197
        $container,
198
        array $definition = [],
199
        LoaderInterface $parent = null
200
    ) {
201
        $this->orm = $orm;
202
203
        //Related record schema
204
        $this->schema = $orm->schema($definition[static::RELATION_TYPE]);
205
206
        $this->container = $container;
207
        $this->definition = $definition;
208
        $this->parent = $parent;
0 ignored issues
show
Documentation Bug introduced by
It seems like $parent can also be of type object<Spiral\ORM\LoaderInterface>. However, the property $parent is declared as type object<Spiral\ORM\Entities\Loader>|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
209
210
        //Compiling options
211
        $this->options['method'] = static::LOAD_METHOD;
212
213
        if (!empty($parent)) {
214
            if (!$parent instanceof Loader || $parent->getDatabase() != $this->getDatabase()) {
215
                //We have to force post-load (separate query) if parent loader database is different
216
                $this->options['method'] = self::POSTLOAD;
217
            }
218
        }
219
220
        $this->dataColumns = array_keys($this->schema[ORM::M_COLUMNS]);
221
    }
222
223
    /**
224
     * Update loader options.
225
     *
226
     * @param array $options
227
     * @return $this
228
     * @throws LoaderException
229
     */
230
    public function setOptions(array $options = [])
231
    {
232
        $this->options = $options + $this->options;
233
234
        if (
235
            $this->isJoinable()
236
            && !empty($this->parent)
237
            && $this->parent->getDatabase() != $this->getDatabase()
238
        ) {
239
            throw new LoaderException("Unable to join tables located in different databases.");
240
        }
241
242
        return $this;
243
    }
244
245
    /**
246
     * Table name loader relates to.
247
     *
248
     * @return mixed
249
     */
250
    public function getTable()
251
    {
252
        return $this->schema[ORM::M_TABLE];
253
    }
254
255
    /**
256
     * Every loader declares an unique alias for it's source table based on options or based on
257
     * position in loaders chain. In addition, every loader responsible for data loading will add
258
     * "_data" postfix to it's alias.
259
     *
260
     * @return string
261
     */
262
    public function getAlias()
263
    {
264
        if (!empty($this->options['using'])) {
265
            //We are using another relation (presumably defined by with() to load data).
266
            return $this->options['using'];
267
        }
268
269
        if (!empty($this->options['alias'])) {
270
            return $this->options['alias'];
271
        }
272
273
        //We are not really worrying about default loader aliases, joiners more important
274
        if ($this->isLoadable()) {
275
            if (!empty($this->alias)) {
276
                //Alias was already created
277
                return $this->alias;
278
            }
279
280
            //New alias is pretty simple and short
281
            return $this->alias = 'd' . decoct(++self::$counter);
282
        }
283
284
        if (empty($this->parent)) {
285
            $alias = $this->getTable();
286
        } elseif ($this->parent instanceof RootLoader) {
287
            //This is first level of relation loading, we can use relation name by itself
288
            $alias = $this->container;
289
        } else {
290
            //Let's use parent alias to continue chain
291
            $alias = $this->parent->getAlias() . '_' . $this->container;
292
        }
293
294
        return $alias;
295
    }
296
297
    /**
298
     * Database name loader relates to.
299
     *
300
     * @return mixed
301
     */
302
    public function getDatabase()
303
    {
304
        return $this->schema[ORM::M_DB];
305
    }
306
307
    /**
308
     * Instance of Dbal\Database data associated with loader instance, used as primary database
309
     * for selector is loader defined as primary selection loader.
310
     *
311
     * @return Database
312
     */
313
    public function dbalDatabase()
314
    {
315
        return $this->orm->database($this->schema[ORM::M_DB]);
316
    }
317
318
    /**
319
     * Get primary key name related to associated record.
320
     *
321
     * @return string|null
322
     */
323
    public function getPrimaryKey()
324
    {
325
        if (!isset($this->schema[ORM::M_PRIMARY_KEY])) {
326
            return null;
327
        }
328
329
        return $this->getAlias() . '.' . $this->schema[ORM::M_PRIMARY_KEY];
330
    }
331
332
    /**
333
     * Pre-load data on inner relation or relation chain. Method automatically called by Selector,
334
     * see load() method.
335
     *
336
     * @see Selector::load()
337
     * @param string $relation Relation name, or chain of relations separated by.
338
     * @param array  $options  Loader options (will be applied to last chain element only).
339
     * @return LoaderInterface
340
     * @throws LoaderException
341
     */
342
    public function loader($relation, array $options = [])
343
    {
344 View Code Duplication
        if (($position = strpos($relation, '.')) !== false) {
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...
345
            //Chain of relations provided
346
            $nested = $this->loader(substr($relation, 0, $position), []);
347
348
            if (empty($nested) || !$nested instanceof self) {
349
                //todo: Think about the options
350
                throw new LoaderException(
351
                    "Only ORM loaders can be used to generate/configure chain of relation loaders."
352
                );
353
            }
354
355
            //Recursively (will work only with ORM loaders).
356
            return $nested->loader(substr($relation, $position + 1), $options);
357
        }
358
359 View Code Duplication
        if (!isset($this->schema[ORM::M_RELATIONS][$relation])) {
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...
360
            $container = $this->container ?: $this->schema[ORM::M_ROLE_NAME];
361
362
            throw new LoaderException(
363
                "Undefined relation '{$relation}' under '{$container}'."
364
            );
365
        }
366
367
        if (isset($this->loaders[$relation])) {
368
            $nested = $this->loaders[$relation];
369
            if (!$nested instanceof self) {
370
                throw new LoaderException(
371
                    "Only ORM loaders can be used to generate/configure chain of relation loaders."
372
                );
373
            }
374
375
            //Updating existed loaded options
376
            $nested->setOptions($options);
377
378
            return $nested;
379
        }
380
381
        $relationOptions = $this->schema[ORM::M_RELATIONS][$relation];
382
383
        //Asking ORM for loader instance
384
        $loader = $this->orm->loader(
385
            $relationOptions[ORM::R_TYPE],
386
            $relation,
387
            $relationOptions[ORM::R_DEFINITION],
388
            $this
389
        );
390
391
        if (!empty($options) && !$loader instanceof self) {
392
            //todo: think about alternatives again
393
            throw new LoaderException(
394
                "Only ORM loaders can be used to generate/configure chain of relation loaders."
395
            );
396
        }
397
398
        $loader->setOptions($options);
399
        $this->loaders[$relation] = $loader;
400
401
        if ($referenceKey = $loader->getReferenceKey()) {
402
            /**
403
             * Inner loader requests parent to pre-collect some keys so it can build tree using
404
             * references without looking up for correct record every time.
405
             */
406
            $this->referenceKeys[] = $referenceKey;
407
            $this->referenceKeys = array_unique($this->referenceKeys);
408
        }
409
410
        return $loader;
411
    }
412
413
    /**
414
     * Filter data on inner relation or relation chain. Method automatically called by Selector,
415
     * see with() method. Logic is identical to loader() method.
416
     *
417
     * @see Selector::load()
418
     * @param string $relation Relation name, or chain of relations separated by.
419
     * @param array  $options  Loader options (will be applied to last chain element only).
420
     * @return Loader
421
     * @throws LoaderException
422
     */
423
    public function joiner($relation, array $options = [])
424
    {
425
        //We have to force joining method for full chain
426
        $options['method'] = self::JOIN;
427
428 View Code Duplication
        if (($position = strpos($relation, '.')) !== false) {
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...
429
            //Chain of relations provided
430
            $nested = $this->joiner(substr($relation, 0, $position), []);
431
            if (empty($nested) || !$nested instanceof self) {
432
                //todo: DRY
433
                throw new LoaderException(
434
                    "Only ORM loaders can be used to generate/configure chain of relation joiners."
435
                );
436
            }
437
438
            //Recursively (will work only with ORM loaders).
439
            return $nested->joiner(substr($relation, $position + 1), $options);
440
        }
441
442 View Code Duplication
        if (!isset($this->schema[ORM::M_RELATIONS][$relation])) {
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...
443
            $container = $this->container ?: $this->schema[ORM::M_ROLE_NAME];
444
445
            throw new LoaderException(
446
                "Undefined relation '{$relation}' under '{$container}'."
447
            );
448
        }
449
450
        if (isset($this->joiners[$relation])) {
451
            //Updating existed joiner options
452
            return $this->joiners[$relation]->setOptions($options);
453
        }
454
455
        $relationOptions = $this->schema[ORM::M_RELATIONS][$relation];
456
457
        $joiner = $this->orm->loader(
458
            $relationOptions[ORM::R_TYPE],
459
            $relation,
460
            $relationOptions[ORM::R_DEFINITION],
461
            $this
462
        );
463
464
        if (!$joiner instanceof self) {
465
            //todo: DRY
466
            throw new LoaderException(
467
                "Only ORM loaders can be used to generate/configure chain of relation joiners."
468
            );
469
        }
470
471
        return $this->joiners[$relation] = $joiner->setOptions($options);
472
    }
473
474
    /**
475
     * {@inheritdoc}
476
     */
477
    public function isMultiple()
478
    {
479
        return static::MULTIPLE;
480
    }
481
482
    /**
483
     * {@inheritdoc}
484
     */
485
    public function getReferenceKey()
486
    {
487
        //In most of cases reference key is inner key name (parent "ID" field name), don't be confused
488
        //by INNER_KEY, remember that we building relation from parent record point of view
489
        return $this->definition[RecordEntity::INNER_KEY];
490
    }
491
492
    /**
493
     * {@inheritdoc}
494
     */
495
    public function aggregatedKeys($referenceKey)
496
    {
497
        if (!isset($this->references[$referenceKey])) {
498
            return [];
499
        }
500
501
        return array_unique(array_keys($this->references[$referenceKey]));
502
    }
503
504
    /**
505
     * Create selector dedicated to load data for current loader.
506
     *
507
     * @return RecordSelector|null
508
     */
509
    public function createSelector()
510
    {
511
        if (!$this->isLoadable()) {
512
            return null;
513
        }
514
515
        $selector = $this->orm->selector($this->definition[static::RELATION_TYPE], $this);
516
517
        //Setting columns to be loaded
518
        $this->configureColumns($selector);
519
520
        foreach ($this->loaders as $loader) {
521
            if ($loader instanceof self) {
522
                //Allowing sub loaders to configure required columns and conditions as well
523
                $loader->configureSelector($selector);
524
            }
525
        }
526
527
        foreach ($this->joiners as $joiner) {
528
            //Joiners must configure selector as well
529
            $joiner->configureSelector($selector);
530
        }
531
532
        return $selector;
533
    }
534
535
    /**
536
     * Configure provided selector with required joins, columns and conditions, in addition method
537
     * must pass configuration to sub loaders.
538
     *
539
     * Method called by Selector when loader set as primary selection loader.
540
     *
541
     * @param RecordSelector $selector
542
     */
543
    public function configureSelector(RecordSelector $selector)
544
    {
545
        if (!$this->isJoinable()) {
546
            //Loader can be used not only for loading but purely for filering
547
            if (empty($this->parent)) {
548
                foreach ($this->loaders as $loader) {
549
                    if ($loader instanceof self) {
550
                        $loader->configureSelector($selector);
551
                    }
552
                }
553
554
                foreach ($this->joiners as $joiner) {
555
                    //Nested joiners
556
                    $joiner->configureSelector($selector);
557
                }
558
            }
559
560
            return;
561
        }
562
563
        if (!$this->configured) {
564
            //We never configured loader columns before
565
            $this->configureColumns($selector);
566
567
            //Inload conditions and etc
568
            if (empty($this->options['using']) && !empty($this->parent)) {
569
                $this->clarifySelector($selector);
570
            }
571
572
            $this->configured = true;
573
        }
574
575
        foreach ($this->loaders as $loader) {
576
            if ($loader instanceof self) {
577
                $loader->configureSelector($selector);
578
            }
579
        }
580
581
        foreach ($this->joiners as $joiner) {
582
            $joiner->configureSelector($selector);
583
        }
584
    }
585
586
    /**
587
     * Implementation specific selector configuration, must create required joins, conditions and
588
     * etc.
589
     *
590
     * @param RecordSelector $selector
591
     */
592
    abstract protected function clarifySelector(RecordSelector $selector);
593
594
    /**
595
     * Parse QueryResult provided by parent loaders and populate data tree. Loader must pass parsing
596
     * to inner loaders also.
597
     *
598
     * @param QueryResult $result
599
     * @param int         $rowsCount
600
     * @return array
601
     */
602
    public function parseResult(QueryResult $result, &$rowsCount)
603
    {
604
        foreach ($result as $row) {
605
            $this->parseRow($row);
606
            $rowsCount++;
607
        }
608
609
        return $this->result;
610
    }
611
612
    /**
613
     * {@inheritdoc}
614
     *
615
     * Method will clarify Loader data tree result using nested loaders.
616
     */
617
    public function loadData()
618
    {
619
        foreach ($this->loaders as $loader) {
620
            if ($loader instanceof self && !$loader->isJoinable()) {
621
                if (!empty($selector = $loader->createSelector())) {
622
                    //Data will be automatically linked via references and mount method
623
                    $selector->fetchData();
624
                }
625
            } else {
626
                //Some other loader type or loader requested separate query to be created
627
                $loader->loadData();
628
            }
629
        }
630
    }
631
632
    /**
633
     * Get compiled data tree, method must be called only if loader were feeded with QueryResult
634
     * using parseResult() method. Attention, method must be called AFTER loadData() with additional
635
     * loaders were executed.
636
     *
637
     * @see loadData()
638
     * @see parseResult()
639
     * @return array
640
     */
641
    public function getResult()
642
    {
643
        return $this->result;
644
    }
645
646
    /**
647
     * {@inheritdoc}
648
     *
649
     * Data will be mounted using references.
650
     */
651
    public function mount($container, $key, $criteria, array &$data, $multiple = false)
652
    {
653
        foreach ($this->references[$key][$criteria] as &$subset) {
654
            if ($multiple) {
655
                if (isset($subset[$container]) && in_array($data, $subset[$container])) {
656
                    unset($subset);
657
                    continue;
658
                }
659
660
                $subset[$container][] = &$data;
661
                unset($subset);
662
663
                continue;
664
            }
665
666
            if (isset($subset[$container])) {
667
                $data = &$subset[$container];
668
            } else {
669
                $subset[$container] = &$data;
670
            }
671
672
            unset($subset);
673
        }
674
    }
675
676
    /**
677
     * {@inheritdoc}
678
     *
679
     * @param bool $reconfigure Use this option to reset configured flag to force query
680
     *                          clarification on next query creation.
681
     */
682
    public function clean($reconfigure = false)
683
    {
684
        $this->duplicates = [];
685
        $this->references = [];
686
        $this->result = [];
687
688
        if ($reconfigure) {
689
            $this->configured = false;
690
        }
691
692
        foreach ($this->loaders as $loader) {
693
            if (!$loader instanceof self) {
694
                continue;
695
            }
696
697
            //POSTLOAD loaders create unique Selector every time, meaning we will have to flush flag
698
            //indicates that associated selector was configured
699
            $loader->clean($reconfigure || !$this->isLoadable());
700
        }
701
    }
702
703
    /**
704
     * Cloning selector presets
705
     */
706
    public function __clone()
707
    {
708
        foreach ($this->loaders as $name => $loader) {
709
            $this->loaders[$name] = clone $loader;
710
        }
711
712
        foreach ($this->joiners as $name => $loader) {
713
            $this->joiners[$name] = clone $loader;
714
        }
715
    }
716
717
    /**
718
     * Destruct loader.
719
     */
720
    public function __destruct()
721
    {
722
        $this->clean();
723
        $this->loaders = [];
724
        $this->joiners = [];
725
    }
726
727
    /**
728
     * Indicates that loader columns must be included into query statement.
729
     *
730
     * @return bool
731
     */
732
    protected function isLoadable()
733
    {
734
        if (!empty($this->parent) && !$this->parent->isLoadable()) {
735
            //If parent not loadable we are no loadable also
736
            return false;
737
        }
738
739
        return $this->options['method'] !== self::JOIN;
740
    }
741
742
    /**
743
     * Indicated that loaded must generate JOIN statement.
744
     *
745
     * @return bool
746
     */
747
    protected function isJoinable()
748
    {
749
        if (!empty($this->options['using'])) {
750
            return true;
751
        }
752
753
        return in_array($this->options['method'], [self::INLOAD, self::JOIN]);
754
    }
755
756
    /**
757
     * If loader is joinable we can calculate join type based on way loader going to be used
758
     * (loading or filtering).
759
     *
760
     * @return string
761
     * @throws LoaderException
762
     */
763
    protected function joinType()
764
    {
765
        if (!$this->isJoinable()) {
766
            throw new LoaderException("Unable to resolve Loader join type, Loader is not joinable.");
767
        }
768
769
        return $this->options['method'] == self::JOIN ? 'INNER' : 'LEFT';
770
    }
771
772
    /**
773
     * Fetch record columns from query row, must use data offset to slice required part of query.
774
     *
775
     * @param array $row
776
     * @return array
777
     */
778
    protected function fetchData(array $row)
779
    {
780
        //Combine column names with sliced piece of row
781
        return array_combine(
782
            $this->dataColumns,
783
            array_slice($row, $this->dataOffset, count($this->dataColumns))
784
        );
785
    }
786
787
    /**
788
     * In many cases (for example if you have inload of HAS_MANY relation) record data can be
789
     * replicated by many result rows (duplicated). To prevent wrong data linking we have to
790
     * deduplicate such records. This is only internal loader functionality and required due data
791
     * tree are built using php references.
792
     *
793
     * Method will return true if data is unique handled before and false in opposite case.
794
     * Provided data array will be automatically linked with it's unique state using references.
795
     *
796
     * @param array $data Reference to parsed record data, reference will be pointed to valid and
797
     *                    existed data segment if such data was already parsed.
798
     * @return bool
799
     */
800
    protected function deduplicate(array &$data)
801
    {
802
        if (isset($this->schema[ORM::M_PRIMARY_KEY])) {
803
            //We can use record id as de-duplication criteria
804
            $criteria = $data[$this->schema[ORM::M_PRIMARY_KEY]];
805
        } else {
806
            //It is recommended to use primary keys in every record as it will speed up de-duplication.
807
            $criteria = serialize($data);
808
        }
809
810
        if (isset($this->duplicates[$criteria])) {
811
            //Duplicate is presented, let's reduplicate
812
            $data = $this->duplicates[$criteria];
813
814
            //Duplicate is presented
815
            return false;
816
        }
817
818
        //Let's force placeholders for every sub loaded
819
        foreach ($this->loaders as $container => $loader) {
820
            $data[$container] = $loader->isMultiple() ? [] : null;
821
        }
822
823
        //Remember record to prevent future duplicates
824
        $this->duplicates[$criteria] = &$data;
825
826
        return true;
827
    }
828
829
    /**
830
     * Generate sql identifier using loader alias and value from relation definition.
831
     *
832
     * Example:
833
     * $this->getKey(Record::OUTER_KEY);
834
     *
835
     * @param string $key
836
     * @return string|null
837
     */
838
    protected function getKey($key)
839
    {
840
        if (!isset($this->definition[$key])) {
841
            return null;
842
        }
843
844
        return $this->getAlias() . '.' . $this->definition[$key];
845
    }
846
847
    /**
848
     * SQL identified to parent record outer key (usually primary key).
849
     *
850
     * @return string
851
     * @throws LoaderException
852
     */
853
    protected function getParentKey()
854
    {
855
        if (empty($this->parent)) {
856
            throw new LoaderException("Unable to get parent key, no parent loader provided.");
857
        }
858
859
        return $this->parent->getAlias() . '.' . $this->definition[RecordEntity::INNER_KEY];
860
    }
861
862
    /**
863
     * Configure columns required for loader data selection.
864
     *
865
     * @param RecordSelector $selector
866
     */
867
    protected function configureColumns(RecordSelector $selector)
868
    {
869
        if (!$this->isLoadable()) {
870
            return;
871
        }
872
873
        $this->dataOffset = $selector->generateColumns($this->getAlias(), $this->dataColumns);
874
    }
875
876
    /**
877
     * Reference criteria is value to be used to mount data into parent loader tree.
878
     *
879
     * Example:
880
     * User has many Posts (relation "posts"), user primary is ID, post inner key pointing to user
881
     * is USER_ID. Post loader must request User data loader to create references based on ID field
882
     * values. Once Post data were parsed we can mount it under parent user using mount method:
883
     *
884
     * $this->parent->mount("posts", "ID", $data["USER_ID"], $data, true); //true = multiple
885
     *
886
     * @see getReferenceKey()
887
     * @param array $data
888
     * @return mixed
889
     */
890 View Code Duplication
    protected function fetchCriteria(array $data)
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...
891
    {
892
        if (!isset($data[$this->definition[RecordEntity::OUTER_KEY]])) {
893
            return null;
894
        }
895
896
        return $data[$this->definition[RecordEntity::OUTER_KEY]];
897
    }
898
899
    /**
900
     * Parse single result row to generate data tree. Must pass parsing to every nested loader.
901
     *
902
     * @param array $row
903
     * @return bool
904
     */
905
    private function parseRow(array $row)
906
    {
907
        if (!$this->isLoadable()) {
908
            //Nothing to parse, we are no waiting for any data
909
            return;
910
        }
911
912
        //Fetching only required part of resulted row
913
        $data = $this->fetchData($row);
914
915
        if (empty($this->parent)) {
916
            if ($this->deduplicate($data)) {
917
                //Yes, this is reference, i'm using this method to build data tree using nested parsers
918
                $this->result[] = &$data;
919
920
                //Registering references to simplify tree compilation for post and inner loaders
921
                $this->collectReferences($data);
922
            }
923
924
            $this->parseNested($row);
925
926
            return;
927
        }
928
929
        if (!$referenceCriteria = $this->fetchCriteria($data)) {
930
            //Relation not loaded
931
            return;
932
        }
933
934
        if ($this->deduplicate($data)) {
935
            //Registering references to simplify tree compilation for post and inner loaders
936
            $this->collectReferences($data);
937
        }
938
939
        //Mounting parsed data into parent under defined container
940
        $this->parent->mount(
941
            $this->container,
942
            $this->getReferenceKey(),
943
            $referenceCriteria,
944
            $data,
945
            static::MULTIPLE
946
        );
947
948
        $this->parseNested($row);
949
    }
950
951
    /**
952
     * Parse data using nested loaders.
953
     *
954
     * @param array $row
955
     */
956
    private function parseNested(array $row)
957
    {
958
        foreach ($this->loaders as $loader) {
959
            if ($loader instanceof self && $loader->isJoinable() && $loader->isLoadable()) {
960
                $loader->parseRow($row);
961
            }
962
        }
963
    }
964
965
    /**
966
     * Create internal references cache based on requested keys. For example, if we have request for
967
     * "id" as reference key, every record will create following structure:
968
     * $this->references[id][ID_VALUE] = ITEM
969
     *
970
     * Only deduplicated data must be collected!
971
     *
972
     * @see deduplicate()
973
     * @param array $data
974
     */
975
    private function collectReferences(array &$data)
976
    {
977
        foreach ($this->referenceKeys as $key) {
978
            //Adding reference(s)
979
            $this->references[$key][$data[$key]][] = &$data;
980
        }
981
    }
982
}
983