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
        /** It's impossible to join with {@see UpdateLoader} because it doesn't produce any SQL */
173 40
        if ($this->parent instanceof UpdateLoader) {
174
            return false;
175
        }
176 4402
177
        if (!empty($this->options['using'])) {
178
            return true;
179
        }
180
181
        return \in_array($this->getMethod(), [self::INLOAD, self::JOIN, self::LEFT_JOIN], true);
182 2490
    }
183
184 2490
    /**
185
     * Indicated that loaded must generate JOIN statement.
186
     */
187
    public function isSubQueried(): bool
188
    {
189
        return $this->getMethod() === self::SUBQUERY;
190 4458
    }
191
192 4458
    /**
193
     * Indication that loader want to load data.
194
     */
195 24
    public function isLoaded(): bool
196
    {
197 24
        return $this->options['load'] || \in_array($this->getMethod(), [self::INLOAD, self::POSTLOAD], true);
198
    }
199
200
    /**
201 24
     * Configure query with conditions, joins and columns.
202 24
     *
203
     * @param array $outerKeys Set of OUTER_KEY values collected by parent loader.
204
     */
205
    public function configureQuery(SelectQuery $query, array $outerKeys = []): SelectQuery
206
    {
207
        if ($this->isLoaded()) {
208
            if ($this->isJoined() || $this->isSubQueried()) {
209
                // mounting the columns to parent query
210 4306
                $this->mountColumns($query, $this->options['minify']);
211
            } else {
212 4306
                // this is initial set of columns (remove all existed)
213 3866
                $this->mountColumns($query, $this->options['minify'], '', true);
214
            }
215 2442
216
            if ($this->options['load'] instanceof ScopeInterface) {
217
                $this->options['load']->apply($this->makeQueryBuilder($query));
218 2474
            }
219
220
            if (\is_callable($this->options['load'], true)) {
221 3866
                ($this->options['load'])($this->makeQueryBuilder($query));
222
            }
223
        }
224
225 3866
        return parent::configureQuery($query);
226 32
    }
227
228
    protected function configureSubQuery(SelectQuery $query): SelectQuery
229
    {
230 4306
        if (!$this->isJoined()) {
231
            return $this->configureQuery($query);
232
        }
233 4306
234
        $loader = new SubQueryLoader($this->ormSchema, $this->sourceProvider, $this->factory, $this, $this->options);
235 4306
        return $loader->configureQuery($query);
236
    }
237 4306
238
    protected function applyScope(SelectQuery $query): SelectQuery
239
    {
240
        $this->scope?->apply($this->makeQueryBuilder($query));
241
242
        return $query;
243 4402
    }
244
245 4402
    /**
246
     * Get load method.
247
     */
248
    protected function getMethod(): int
249
    {
250
        return $this->options['method'];
251 2338
    }
252
253 2338
    /**
254
     * Create relation specific select query.
255
     */
256
    protected function initQuery(): SelectQuery
257
    {
258
        return $this->source->getDatabase()->select()->from($this->getJoinTable());
259 4458
    }
260
261 4458
    /**
262 2522
     * Calculate table alias.
263
     */
264
    protected function calculateAlias(AbstractLoader $parent): string
265 3930
    {
266
        if (!empty($this->options['as'])) {
267 3930
            return $this->options['as'];
268
        }
269 1562
270
        $alias = $parent->getAlias() . '_' . $this->name;
271
272 2986
        if ($this->isLoaded() && $this->isJoined()) {
273
            // to avoid collisions
274
            return 'l_' . $alias;
275
        }
276
277
        return $alias;
278
    }
279
280
    /**
281
     * Generate sql identifier using loader alias and value from relation definition. Key name to be
282 312
     * fetched from schema.
283
     *
284 312
     * Example:
285
     *
286
     *     $this->getKey(Relation::OUTER_KEY);
287
     */
288 312
    protected function localKey(string|int $key): ?string
289
    {
290
        if (empty($this->schema[$key])) {
291
            return null;
292
        }
293
294
        return $this->getAlias() . '.' . $this->fieldAlias($this->schema[$key]);
295
    }
296
297
    /**
298
     * Get parent identifier based on relation configuration key.
299 2194
     */
300
    protected function parentKey(string|int $key): string
301 2194
    {
302
        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

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