JoinableLoader   B
last analyzed

Complexity

Total Complexity 48

Size/Duplication

Total Lines 298
Duplicated Lines 0 %

Test Coverage

Coverage 87.5%

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 93
dl 0
loc 298
ccs 84
cts 96
cp 0.875
rs 8.5599
c 2
b 0
f 1
wmc 48

18 Methods

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

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
        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