ManyToManyLoader::loadChild()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 2
dl 0
loc 8
ccs 5
cts 5
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cycle\ORM\Select\Loader;
6
7
use Cycle\Database\Injection\Parameter;
8
use Cycle\Database\Query\SelectQuery;
9
use Cycle\ORM\Exception\LoaderException;
10
use Cycle\ORM\FactoryInterface;
11
use Cycle\ORM\Parser\AbstractNode;
12
use Cycle\ORM\Parser\SingularNode;
13
use Cycle\ORM\Service\SourceProviderInterface;
14
use Cycle\ORM\Relation;
15
use Cycle\ORM\SchemaInterface;
16
use Cycle\ORM\Select\JoinableLoader;
17
use Cycle\ORM\Select\LoaderInterface;
18
use Cycle\ORM\Select\Traits\OrderByTrait;
19
use Cycle\ORM\Select\Traits\WhereTrait;
20
21
/**
22
 * @internal
23
 */
24
class ManyToManyLoader extends JoinableLoader
25
{
26
    use OrderByTrait;
27
    use WhereTrait;
28
29
    /**
30
     * Default set of relation options. Child implementation might defined their of default options.
31
     */
32
    protected array $options = [
33
        'load' => false,
34
        'scope' => true,
35
        'method' => self::POSTLOAD,
36
        'minify' => true,
37
        'as' => null,
38
        'using' => null,
39
        'where' => null,
40
        'orderBy' => null,
41
        'pivot' => null,
42
    ];
43
44
    protected PivotLoader $pivot;
45
46 816
    public function __construct(
47
        SchemaInterface $ormSchema,
48
        SourceProviderInterface $sourceProvider,
49
        FactoryInterface $factory,
50
        string $name,
51
        string $target,
52
        array $schema,
53
    ) {
54 816
        parent::__construct($ormSchema, $sourceProvider, $factory, $name, $target, $schema);
55 816
        $this->pivot = new PivotLoader(
56
            $ormSchema,
57
            $sourceProvider,
58
            $factory,
59
            'pivot',
60 816
            $schema[Relation::THROUGH_ENTITY],
61
            $schema,
62
        );
63 816
        $this->options['where'] = $schema[Relation::WHERE] ?? [];
64 816
        $this->options['orderBy'] = $schema[Relation::ORDER_BY] ?? [];
65
    }
66
67
    public function withContext(LoaderInterface $parent, array $options = []): static
68
    {
69
        /** @var ManyToManyLoader $loader */
70 816
        $loader = parent::withContext($parent, $options);
71
        $loader->pivot = $loader->pivot->withContext(
72 816
            $loader,
73 816
            [
74
                'load' => $loader->isLoaded(),
75
                'method' => $options['method'] ?? self::JOIN,
76 816
            ] + ($options['pivot'] ?? []),
77
        );
78
79 816
        return $loader;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $loader returns the type Cycle\ORM\Select\AbstractLoader which includes types incompatible with the type-hinted return Cycle\ORM\Select\Loader\ManyToManyLoader.
Loading history...
80 816
    }
81
82
    public function loadRelation(
83 816
        string|LoaderInterface $relation,
84 816
        array $options,
85 816
        bool $join = false,
86
        bool $load = false,
87
    ): LoaderInterface {
88 816
        if ($relation === '@' || $relation === '@.@') {
89
            unset($options['method']);
90
            if ($options !== []) {
91 88
                // re-configure
92
                $this->pivot = $this->pivot->withContext($this, $options);
93
            }
94
95
            return $this->pivot;
96
        }
97 88
98 64
        return parent::loadRelation($relation, $options, $join, $load);
99 64
    }
100
101
    public function configureQuery(SelectQuery $query, array $outerKeys = []): SelectQuery
102
    {
103
        if ($this->isLoaded() && $this->isJoined() && (int) $query->getLimit() !== 0) {
104 64
            throw new LoaderException('Unable to load data using join with limit on parent query');
105
        }
106
107 72
        if ($this->options['using'] !== null) {
108
            // use pre-defined query
109
            return parent::configureQuery($this->pivot->configureQuery($query), $outerKeys);
110 816
        }
111
112 816
113 8
        $localPrefix = $this->getAlias() . '.';
114
        $pivotPrefix = $this->pivot->getAlias() . '.';
115
116 808
        // Manually join pivoted table
117
        if ($this->isJoined()) {
118
            $parentKeys = (array) $this->schema[Relation::INNER_KEY];
119
            $throughOuterKeys = (array) $this->pivot->schema[Relation::THROUGH_OUTER_KEY];
120
            $parentPrefix = $this->parent->getAlias() . '.';
0 ignored issues
show
Bug introduced by
The method getAlias() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

120
            $parentPrefix = $this->parent->/** @scrutinizer ignore-call */ getAlias() . '.';

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...
121
            $on = [];
122 808
            foreach ((array) $this->pivot->schema[Relation::THROUGH_INNER_KEY] as $i => $key) {
123 808
                $field = $pivotPrefix . $this->pivot->fieldAlias($key);
124
                $on[$field] = $parentPrefix . $this->parent->fieldAlias($parentKeys[$i]);
125
            }
126 808
127 192
            $query->join(
128 192
                $this->getJoinMethod(),
129 192
                $this->pivot->getJoinTable(),
130 192
            )->on($on);
131 192
132 192
            $on = [];
133 192
            foreach ((array) $this->schema[Relation::OUTER_KEY] as $i => $key) {
134
                $field = $localPrefix . $this->fieldAlias($key);
135
                $on[$field] = $pivotPrefix . $this->pivot->fieldAlias($throughOuterKeys[$i]);
136 192
            }
137 192
138 192
            $query->join(
139 192
                $this->getJoinMethod(),
140
                $this->getJoinTable(),
141 192
            )->on($on);
142 192
        } elseif ($outerKeys !== []) {
143 192
            // reset all the columns when query is isolated (we have to do it manually
144 192
            // since underlying loader believes it's loaded)
145
            $query->columns([]);
146
147 192
            $outerKeyList = (array) $this->schema[Relation::OUTER_KEY];
148 192
            $on = [];
149 192
            foreach ((array) $this->pivot->schema[Relation::THROUGH_OUTER_KEY] as $i => $key) {
150 192
                $field = $pivotPrefix . $this->pivot->fieldAlias($key);
151 640
                $on[$field] = $localPrefix . $this->fieldAlias($outerKeyList[$i]);
152
            }
153
154 640
            $query->join(
155
                $this->getJoinMethod(),
156 640
                $this->pivot->getJoinTable(),
157 640
            )->on($on);
158 640
159 640
            $fields = [];
160 640
            foreach ((array) $this->pivot->schema[Relation::THROUGH_INNER_KEY] as $key) {
161
                $fields[] = $pivotPrefix . $this->pivot->fieldAlias($key);
162
            }
163 640
164 640
            if (\count($fields) === 1) {
165 640
                $query->andWhere($fields[0], 'IN', new Parameter(\array_column($outerKeys, \key($outerKeys[0]))));
166 640
            } else {
167
                $query->andWhere(
168 640
                    static function (SelectQuery $select) use ($outerKeys, $fields): void {
169 640
                        foreach ($outerKeys as $set) {
170 640
                            $select->orWhere(\array_combine($fields, \array_values($set)));
171
                        }
172
                    },
173 640
                );
174 576
            }
175
        }
176 64
177 64
        // user specified WHERE conditions
178 64
        $this->setWhere(
179 64
            $query,
180
            $this->isJoined() ? 'onWhere' : 'where',
181
            $this->options['where'] ?? $this->schema[Relation::WHERE] ?? [],
182
        );
183
184
        // user specified ORDER_BY rules
185
        $this->setOrderBy(
186
            $query,
187 808
            $this->getAlias(),
188
            $this->options['orderBy'] ?? $this->schema[Relation::ORDER_BY] ?? [],
189 808
        );
190 808
191
        return parent::configureQuery($this->pivot->configureQuery($query));
192
    }
193
194 808
    public function createNode(): AbstractNode
195
    {
196 808
        $node = $this->pivot->createNode();
197 808
        $node->joinNode('@', parent::createNode());
198
199
        return $node;
200 808
    }
201
202
    /**
203 768
     * Make sure that pivot loader is always carried with parent relation.
204
     */
205 768
    public function __clone()
206 768
    {
207
        parent::__clone();
208 768
        $this->pivot = clone $this->pivot;
209
    }
210
211 760
    protected function loadChild(AbstractNode $node, bool $includeRole = false): void
212
    {
213 760
        $rootNode = $node->getNode('@');
214 760
        foreach ($this->load as $relation => $loader) {
215 24
            $loader->loadData($rootNode->getNode($relation), $includeRole);
216
        }
217
218 760
        $this->pivot->loadChild($node, $includeRole);
219
    }
220
221 760
    protected function mountColumns(
222
        SelectQuery $query,
223
        bool $minify = false,
224
        string $prefix = '',
225
        bool $overwrite = false,
0 ignored issues
show
Unused Code introduced by
The parameter $overwrite is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

225
        /** @scrutinizer ignore-unused */ bool $overwrite = false,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
226
    ): SelectQuery {
227
        // columns are reset on earlier stage to allow pivot loader mount it's own aliases
228 760
        return parent::mountColumns($query, $minify, $prefix, false);
229
    }
230
231 768
    protected function initNode(): AbstractNode
232
    {
233 768
        return new SingularNode(
234 768
            $this->columnNames(),
235 768
            (array) $this->define(SchemaInterface::PRIMARY_KEY),
236 768
            (array) $this->schema[Relation::OUTER_KEY],
237 768
            (array) $this->schema[Relation::THROUGH_OUTER_KEY],
238
        );
239
    }
240
}
241