JoinableLoader::withContext()   B
last analyzed

Complexity

Conditions 10
Paths 25

Size

Total Lines 42
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 10.6913

Importance

Changes 0
Metric Value
cc 10
eloc 21
c 0
b 0
f 0
nc 25
nop 2
dl 0
loc 42
ccs 17
cts 21
cp 0.8095
crap 10.6913
rs 7.6666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cycle\ORM\Select;
6
7
use Cycle\Database\Query\SelectQuery;
8
use Cycle\Database\StatementInterface;
9
use Cycle\ORM\Exception\LoaderException;
10
use Cycle\ORM\FactoryInterface;
11
use Cycle\ORM\Parser\AbstractNode;
12
use Cycle\ORM\Service\SourceProviderInterface;
13
use Cycle\ORM\SchemaInterface;
14
use Cycle\ORM\Select\Loader\SubQueryLoader;
15
use Cycle\ORM\Select\Traits\ColumnsTrait;
16
use Cycle\ORM\Select\Traits\ScopeTrait;
17
18
/**
19
 * Provides ability to load relation data in a form of JOIN or external query.
20
 *
21
 * @internal
22
 */
23
abstract class JoinableLoader extends AbstractLoader implements JoinableInterface
24
{
25
    use ColumnsTrait;
26
    use ScopeTrait;
27
28
    /**
29
     * Default set of relation options. Child implementation might defined their of default options.
30
     */
31
    protected array $options = [
32
        // load relation data
33
        'load' => false,
34
35
        // true or instance to enable, false or null to disable
36
        'scope' => true,
37
38
        // scope to be used for the relation
39
        'method' => null,
40
41
        // load method, see AbstractLoader constants
42
        'minify' => true,
43
44
        // when true all loader columns will be minified (only for loading)
45
        'as' => null,
46
47
        // table alias
48
        'using' => null,
49
50
        // alias used by another relation
51
        'where' => null,
52
53
        // where conditions (if any)
54
    ];
55
56
    /**
57
     * Eager relations and inheritance hierarchies has been loaded
58
     */
59
    private bool $eagerLoaded = false;
60
61 4458
    public function __construct(
62
        SchemaInterface $ormSchema,
63
        SourceProviderInterface $sourceProvider,
64
        FactoryInterface $factory,
65
        protected string $name,
66
        string $target,
67
        protected array $schema,
68
    ) {
69 4458
        parent::__construct($ormSchema, $sourceProvider, $factory, $target);
70 4458
        $this->columns = $this->normalizeColumns($this->define(SchemaInterface::COLUMNS));
71
    }
72
73
    /**
74
     * Relation table alias.
75
     */
76 4322
    public function getAlias(): string
77
    {
78 4322
        if ($this->options['using'] !== null) {
79
            //We are using another relation (presumably defined by with() to load data).
80 40
            return $this->options['using'];
81
        }
82
83 4322
        if ($this->options['as'] !== null) {
84 4322
            return $this->options['as'];
85
        }
86
87
        throw new LoaderException('Unable to resolve loader alias.');
88
    }
89
90 4458
    public function withContext(LoaderInterface $parent, array $options = []): static
91
    {
92
        /**
93
         * @var AbstractLoader $parent
94
         * @var self $loader
95
         */
96 4458
        $loader = parent::withContext($parent, $options);
97
98 4458
        if ($loader->source->getDatabase() !== $parent->source->getDatabase()) {
99
            if ($loader->isJoined()) {
100
                throw new LoaderException('Unable to join tables located in different databases');
101
            }
102
103
            // loader is not joined, let's make sure that POSTLOAD is used
104
            if ($this->isLoaded()) {
105
                $loader->options['method'] = self::POSTLOAD;
106
            }
107
        }
108
109
        //Calculate table alias
110 4458
        $loader->options['as'] = $loader->calculateAlias($parent);
111
112 4458
        if (\array_key_exists('scope', $options)) {
113 256
            if ($loader->options['scope'] instanceof ScopeInterface) {
114 184
                $loader->setScope($loader->options['scope']);
115 72
            } elseif (\is_string($loader->options['scope'])) {
116 256
                $loader->setScope($this->factory->make($loader->options['scope']));
117
            }
118
        } else {
119 4426
            $loader->setScope($this->source->getScope());
120
        }
121
122 4458
        if (!$loader->eagerLoaded && $loader->isLoaded()) {
123 4018
            $loader->eagerLoaded = true;
124 4018
            $loader->inherit = null;
125 4018
            $loader->subclasses = [];
126 4018
            foreach ($loader->getEagerLoaders() as $relation) {
127 872
                $loader->loadRelation($relation, [], false, true);
128
            }
129
        }
130
131 4458
        return $loader;
132
    }
133
134 3842
    public function loadData(AbstractNode $node, bool $includeRole = false): void
135
    {
136 3842
        if ($this->isJoined() || !$this->isLoaded()) {
137
            // load data for all nested relations
138 2114
            parent::loadData($node, $includeRole);
139
140 2114
            return;
141
        }
142
143
        // Get list of reference key values aggregated by parent.
144 2378
        $references = $node->getReferenceValues();
145 2378
        if ($references === []) {
146
            // nothing found at parent level, unable to create sub query
147 184
            return;
148
        }
149
150
        //Ensure all nested relations
151 2338
        $statement = $this->configureQuery($this->initQuery(), $references)->run();
152
153 2338
        foreach ($statement->fetchAll(StatementInterface::FETCH_NUM) as $row) {
154
            try {
155 2282
                $node->parseRow(0, $row);
156
            } catch (\Throwable $e) {
157
                throw $e;
158
            }
159
        }
160
161 2338
        $statement->close();
162
163
        // load data for all nested relations
164 2338
        parent::loadData($node, $includeRole);
165
    }
166
167
    /**
168
     * Indicated that loaded must generate JOIN statement.
169
     */
170 4402
    public function isJoined(): bool
171
    {
172 4402
        if (!empty($this->options['using'])) {
173 40
            return true;
174
        }
175
176 4402
        return \in_array($this->getMethod(), [self::INLOAD, self::JOIN, self::LEFT_JOIN], true);
177
    }
178
179
    /**
180
     * Indicated that loaded must generate JOIN statement.
181
     */
182 2490
    public function isSubQueried(): bool
183
    {
184 2490
        return $this->getMethod() === self::SUBQUERY;
185
    }
186
187
    /**
188
     * Indication that loader want to load data.
189
     */
190 4458
    public function isLoaded(): bool
191
    {
192 4458
        return $this->options['load'] || \in_array($this->getMethod(), [self::INLOAD, self::POSTLOAD], true);
193
    }
194
195 24
    /**
196
     * Configure query with conditions, joins and columns.
197 24
     *
198
     * @param array $outerKeys Set of OUTER_KEY values collected by parent loader.
199
     */
200
    public function configureQuery(SelectQuery $query, array $outerKeys = []): SelectQuery
201 24
    {
202 24
        if ($this->isLoaded()) {
203
            if ($this->isJoined() || $this->isSubQueried()) {
204
                // mounting the columns to parent query
205
                $this->mountColumns($query, $this->options['minify']);
206
            } else {
207
                // this is initial set of columns (remove all existed)
208
                $this->mountColumns($query, $this->options['minify'], '', true);
209
            }
210 4306
211
            if ($this->options['load'] instanceof ScopeInterface) {
212 4306
                $this->options['load']->apply($this->makeQueryBuilder($query));
213 3866
            }
214
215 2442
            if (\is_callable($this->options['load'], true)) {
216
                ($this->options['load'])($this->makeQueryBuilder($query));
217
            }
218 2474
        }
219
220
        return parent::configureQuery($query);
221 3866
    }
222
223
    protected function configureSubQuery(SelectQuery $query): SelectQuery
224
    {
225 3866
        if (!$this->isJoined()) {
226 32
            return $this->configureQuery($query);
227
        }
228
229
        $loader = new SubQueryLoader($this->ormSchema, $this->sourceProvider, $this->factory, $this, $this->options);
230 4306
        return $loader->configureQuery($query);
231
    }
232
233 4306
    protected function applyScope(SelectQuery $query): SelectQuery
234
    {
235 4306
        $this->scope?->apply($this->makeQueryBuilder($query));
236
237 4306
        return $query;
238
    }
239
240
    /**
241
     * Get load method.
242
     */
243 4402
    protected function getMethod(): int
244
    {
245 4402
        return $this->options['method'];
246
    }
247
248
    /**
249
     * Create relation specific select query.
250
     */
251 2338
    protected function initQuery(): SelectQuery
252
    {
253 2338
        return $this->source->getDatabase()->select()->from($this->getJoinTable());
254
    }
255
256
    /**
257
     * Calculate table alias.
258
     */
259 4458
    protected function calculateAlias(AbstractLoader $parent): string
260
    {
261 4458
        if (!empty($this->options['as'])) {
262 2522
            return $this->options['as'];
263
        }
264
265 3930
        $alias = $parent->getAlias() . '_' . $this->name;
266
267 3930
        if ($this->isLoaded() && $this->isJoined()) {
268
            // to avoid collisions
269 1562
            return 'l_' . $alias;
270
        }
271
272 2986
        return $alias;
273
    }
274
275
    /**
276
     * Generate sql identifier using loader alias and value from relation definition. Key name to be
277
     * fetched from schema.
278
     *
279
     * Example:
280
     *
281
     *     $this->getKey(Relation::OUTER_KEY);
282 312
     */
283
    protected function localKey(string|int $key): ?string
284 312
    {
285
        if (empty($this->schema[$key])) {
286
            return null;
287
        }
288 312
289
        return $this->getAlias() . '.' . $this->fieldAlias($this->schema[$key]);
290
    }
291
292
    /**
293
     * Get parent identifier based on relation configuration key.
294
     */
295
    protected function parentKey(string|int $key): string
296
    {
297
        return $this->parent->getAlias() . '.' . $this->parent->fieldAlias($this->schema[$key]);
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

297
        return $this->parent->/** @scrutinizer ignore-call */ getAlias() . '.' . $this->parent->fieldAlias($this->schema[$key]);

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...
298
    }
299 2194
300
    protected function getJoinMethod(): string
301 2194
    {
302
        return $this->getMethod() == self::JOIN ? 'INNER' : 'LEFT';
303
    }
304
305
    /**
306
     * Joined table name and alias.
307 4306
     */
308
    protected function getJoinTable(): string
309 4306
    {
310
        return "{$this->define(SchemaInterface::TABLE)} AS {$this->getAlias()}";
311
    }
312 1232
313
    private function makeQueryBuilder(SelectQuery $query): QueryBuilder
314 1232
    {
315 1232
        $builder = new QueryBuilder($query, $this);
316 616
        if ($this->isJoined()) {
317
            return $builder->withForward('onWhere');
318
        }
319 712
320
        return $builder;
321
    }
322
}
323