JoinableLoader   B
last analyzed

Complexity

Total Complexity 49

Size/Duplication

Total Lines 303
Duplicated Lines 0 %

Test Coverage

Coverage 87.5%

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 95
dl 0
loc 303
ccs 84
cts 96
cp 0.875
rs 8.48
c 2
b 0
f 1
wmc 49

18 Methods

Rating   Name   Duplication   Size   Complexity  
A loadData() 0 31 6
A __construct() 0 10 1
A getAlias() 0 12 3
B withContext() 0 42 10
A isJoined() 0 12 3
A getJoinTable() 0 3 1
A applyScope() 0 5 1
A getJoinMethod() 0 3 2
A getMethod() 0 3 1
A isSubQueried() 0 3 1
A configureSubQuery() 0 8 2
A calculateAlias() 0 14 4
A parentKey() 0 3 1
A configureQuery() 0 21 6
A makeQueryBuilder() 0 8 2
A localKey() 0 7 2
A isLoaded() 0 3 2
A initQuery() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like JoinableLoader often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use JoinableLoader, and based on these observations, apply Extract Interface, too.

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