Passed
Push — master ( 56439d...ee3a42 )
by Anton
02:21
created

JoinableLoader   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 323
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 86
dl 0
loc 323
rs 8.8798
c 1
b 0
f 1
wmc 44

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A getAlias() 0 12 3
A isJoined() 0 7 2
A loadData() 0 26 5
A isLoaded() 0 3 2
B withContext() 0 39 9
A applyConstrain() 0 12 3
A getJoinTable() 0 3 1
A getJoinMethod() 0 3 2
A getMethod() 0 3 1
A getColumns() 0 3 1
A calculateAlias() 0 14 4
A parentKey() 0 3 1
A configureQuery() 0 26 6
A localKey() 0 7 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
 * Cycle DataMapper ORM
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
declare(strict_types=1);
9
10
namespace Cycle\ORM\Select;
11
12
use Cycle\ORM\Exception\LoaderException;
13
use Cycle\ORM\ORMInterface;
14
use Cycle\ORM\Parser\AbstractNode;
15
use Cycle\ORM\Schema;
16
use Cycle\ORM\Select\Traits\ColumnsTrait;
17
use Cycle\ORM\Select\Traits\ConstrainTrait;
18
use Spiral\Database\Query\SelectQuery;
19
use Spiral\Database\StatementInterface;
20
21
/**
22
 * Provides ability to load relation data in a form of JOIN or external query.
23
 */
24
abstract class JoinableLoader extends AbstractLoader implements JoinableInterface
25
{
26
    use ColumnsTrait, ConstrainTrait;
27
28
    /**
29
     * Default set of relation options. Child implementation might defined their of default options.
30
     *
31
     * @var array
32
     */
33
    protected $options = [
34
        // load relation data
35
        'load'      => false,
36
37
        // true or instance to enable, false or null to disable
38
        'constrain' => true,
39
40
        // scope to be used for the relation
41
        'method'    => null,
42
43
        // load method, see AbstractLoader constants
44
        'minify'    => true,
45
46
        // when true all loader columns will be minified (only for loading)
47
        'as'        => null,
48
49
        // table alias
50
        'using'     => null,
51
52
        // alias used by another relation
53
        'where'     => null,
54
55
        // where conditions (if any)
56
    ];
57
58
    /** @var string */
59
    protected $name;
60
61
    /** @var array */
62
    protected $schema;
63
64
    /**
65
     * @param ORMInterface $orm
66
     * @param string       $name
67
     * @param string       $target
68
     * @param array        $schema
69
     */
70
    public function __construct(ORMInterface $orm, string $name, string $target, array $schema)
71
    {
72
        parent::__construct($orm, $target);
73
74
        $this->name = $name;
75
        $this->schema = $schema;
76
    }
77
78
    /**
79
     * Relation table alias.
80
     *
81
     * @return string
82
     */
83
    public function getAlias(): string
84
    {
85
        if (!empty($this->options['using'])) {
86
            //We are using another relation (presumably defined by with() to load data).
87
            return $this->options['using'];
88
        }
89
90
        if (!empty($this->options['as'])) {
91
            return $this->options['as'];
92
        }
93
94
        throw new LoaderException("Unable to resolve loader alias");
95
    }
96
97
    /**
98
     * {@inheritdoc}
99
     */
100
    public function withContext(LoaderInterface $parent, array $options = []): LoaderInterface
101
    {
102
        /**
103
         * @var AbstractLoader $parent
104
         * @var self           $loader
105
         */
106
        $loader = parent::withContext($parent, $options);
107
108
        if ($loader->getSource()->getDatabase() !== $parent->getSource()->getDatabase()) {
109
            if ($loader->isJoined()) {
110
                throw new LoaderException("Unable to join tables located in different databases");
111
            }
112
113
            // loader is not joined, let's make sure that POSTLOAD is used
114
            if ($this->isLoaded()) {
115
                $loader->options['method'] = self::POSTLOAD;
116
            }
117
        }
118
119
        //Calculate table alias
120
        $loader->options['as'] = $loader->calculateAlias($parent);
121
122
        if (array_key_exists('constrain', $options)) {
123
            if ($loader->options['constrain'] instanceof ConstrainInterface) {
124
                $loader->setConstrain($loader->options['constrain']);
125
            } elseif (is_string($loader->options['constrain'])) {
126
                $loader->setConstrain($this->orm->getFactory()->make($loader->options['constrain']));
0 ignored issues
show
Bug introduced by
The method getFactory() does not exist on Cycle\ORM\Select\SourceProviderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Cycle\ORM\Select\SourceProviderInterface. ( Ignorable by Annotation )

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

126
                $loader->setConstrain($this->orm->/** @scrutinizer ignore-call */ getFactory()->make($loader->options['constrain']));
Loading history...
127
            }
128
        } else {
129
            $loader->setConstrain($this->getSource()->getConstrain());
130
        }
131
132
        if ($this->isLoaded()) {
133
            foreach ($loader->getEagerRelations() as $relation) {
134
                $loader->loadRelation($relation, [], false, true);
135
            }
136
        }
137
138
        return $loader;
139
    }
140
141
    /**
142
     * {@inheritdoc}
143
     */
144
    public function loadData(AbstractNode $node)
145
    {
146
        if ($this->isJoined() || !$this->isLoaded()) {
147
            // load data for all nested relations
148
            parent::loadData($node);
149
150
            return;
151
        }
152
153
        $references = $node->getReferences();
154
        if ($references === []) {
155
            // nothing found at parent level, unable to create sub query
156
            return;
157
        }
158
159
        //Ensure all nested relations
160
        $statement = $this->configureQuery($this->initQuery(), $references)->run();
161
162
        foreach ($statement->fetchAll(StatementInterface::FETCH_NUM) as $row) {
163
            $node->parseRow(0, $row);
164
        }
165
166
        $statement->close();
167
168
        // load data for all nested relations
169
        parent::loadData($node);
170
    }
171
172
    /**
173
     * Indicated that loaded must generate JOIN statement.
174
     *
175
     * @return bool
176
     */
177
    public function isJoined(): bool
178
    {
179
        if (!empty($this->options['using'])) {
180
            return true;
181
        }
182
183
        return in_array($this->getMethod(), [self::INLOAD, self::JOIN, self::LEFT_JOIN]);
184
    }
185
186
    /**
187
     * Indication that loader want to load data.
188
     *
189
     * @return bool
190
     */
191
    public function isLoaded(): bool
192
    {
193
        return $this->options['load'] || in_array($this->getMethod(), [self::INLOAD, self::POSTLOAD]);
194
    }
195
196
    /**
197
     * Configure query with conditions, joins and columns.
198
     *
199
     * @param SelectQuery $query
200
     * @param array       $outerKeys Set of OUTER_KEY values collected by parent loader.
201
     * @return SelectQuery
202
     */
203
    public function configureQuery(SelectQuery $query, array $outerKeys = []): SelectQuery
204
    {
205
        if ($this->isLoaded()) {
206
            if ($this->isJoined()) {
207
                if ($query->getLimit() != 0) {
208
                    throw new LoaderException("Unable to load data using join with limit on parent query");
209
                }
210
211
                // mounting the columns to parent query
212
                $this->mountColumns($query, $this->options['minify']);
213
            } else {
214
                // this is initial set of columns (remove all existed)
215
                $this->mountColumns($query, $this->options['minify'], '', true);
216
            }
217
218
            // custom load constrains
219
            if ($this->options['load'] instanceof \Closure) {
220
221
                $proxy = new QueryBuilder($this->orm, $query, $this);
222
                $proxy = $proxy->withForward($this->isJoined() ? 'onWhere' : 'where');
223
224
                call_user_func($this->options['load'], $proxy);
225
            }
226
        }
227
228
        return parent::configureQuery($query);
229
    }
230
231
    /**
232
     * @param SelectQuery $query
233
     * @return SelectQuery
234
     */
235
    protected function applyConstrain(SelectQuery $query): SelectQuery
236
    {
237
        if ($this->constrain !== null) {
238
            $builder = new QueryBuilder($this->orm, $query, $this);
239
            if ($this->isJoined()) {
240
                $builder = $builder->withForward('onWhere');
241
            }
242
243
            $this->constrain->apply($builder);
244
        }
245
246
        return $query;
247
    }
248
249
    /**
250
     * Get load method.
251
     *
252
     * @return int
253
     */
254
    protected function getMethod(): int
255
    {
256
        return $this->options['method'];
257
    }
258
259
    /**
260
     * Create relation specific select query.
261
     *
262
     * @return SelectQuery
263
     */
264
    protected function initQuery(): SelectQuery
265
    {
266
        return $this->getSource()->getDatabase()->select()->from($this->getJoinTable());
267
    }
268
269
    /**
270
     * Calculate table alias.
271
     *
272
     * @param AbstractLoader $parent
273
     * @return string
274
     */
275
    protected function calculateAlias(AbstractLoader $parent): string
276
    {
277
        if (!empty($this->options['as'])) {
278
            return $this->options['as'];
279
        }
280
281
        $alias = $parent->getAlias() . '_' . $this->name;
282
283
        if ($this->isLoaded() && $this->isJoined()) {
284
            // to avoid collisions
285
            return 'l_' . $alias;
286
        }
287
288
        return $alias;
289
    }
290
291
    /**
292
     * Generate sql identifier using loader alias and value from relation definition. Key name to be
293
     * fetched from schema.
294
     *
295
     * Example:
296
     * $this->getKey(Relation::OUTER_KEY);
297
     *
298
     * @param mixed $key
299
     * @return string|null
300
     */
301
    protected function localKey($key): ?string
302
    {
303
        if (empty($this->schema[$key])) {
304
            return null;
305
        }
306
307
        return $this->getAlias() . '.' . $this->fieldAlias($this->schema[$key]);
308
    }
309
310
    /**
311
     * Get parent identifier based on relation configuration key.
312
     *
313
     * @param mixed $key
314
     * @return string
315
     */
316
    protected function parentKey($key): string
317
    {
318
        return $this->parent->getAlias() . '.' . $this->parent->fieldAlias($this->schema[$key]);
319
    }
320
321
    /**
322
     * @return string
323
     */
324
    protected function getJoinMethod(): string
325
    {
326
        return $this->getMethod() == self::JOIN ? 'INNER' : 'LEFT';
327
    }
328
329
    /**
330
     * Joined table name and alias.
331
     *
332
     * @return string
333
     */
334
    protected function getJoinTable(): string
335
    {
336
        return "{$this->define(Schema::TABLE)} AS {$this->getAlias()}";
337
    }
338
339
    /**
340
     * Relation columns.
341
     *
342
     * @return array
343
     */
344
    protected function getColumns(): array
345
    {
346
        return $this->define(Schema::COLUMNS);
347
    }
348
}
349