Passed
Push — master ( 0ea13e...268b05 )
by Anton
01:37
created

JoinableLoader   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 304
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 78
dl 0
loc 304
rs 9.2
c 0
b 0
f 0
wmc 40

16 Methods

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

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