RelationLoader   A
last analyzed

Complexity

Total Complexity 30

Size/Duplication

Total Lines 265
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 0
Metric Value
wmc 30
lcom 1
cbo 8
dl 0
loc 265
rs 10
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A withContext() 0 24 4
A isJoined() 0 8 2
A isLoaded() 0 4 2
A loadData() 0 28 5
A configureQuery() 0 14 3
A getAlias() 0 13 3
A getColumns() 0 4 1
A getMethod() 0 4 1
A localKey() 0 8 2
A parentKey() 0 4 1
A ensureAlias() 0 13 4
A createQuery() 0 6 1
1
<?php
2
/**
3
 * Spiral, Core Components
4
 *
5
 * @author Wolfy-J
6
 */
7
8
namespace Spiral\ORM\Entities\Loaders;
9
10
use Spiral\Database\Builders\SelectQuery;
11
use Spiral\ORM\Entities\Loaders\Traits\ColumnsTrait;
12
use Spiral\ORM\Entities\Nodes\AbstractNode;
13
use Spiral\ORM\Exceptions\LoaderException;
14
use Spiral\ORM\LoaderInterface;
15
use Spiral\ORM\ORMInterface;
16
use Spiral\ORM\Record;
17
18
/**
19
 * Provides ability to load relation data in a form of JOIN or external query.
20
 */
21
abstract class RelationLoader extends AbstractLoader
22
{
23
    use ColumnsTrait;
24
25
    /**
26
     * Used to create unique set of aliases for loaded relations.
27
     *
28
     * @var int
29
     */
30
    private static $countLevels = 0;
31
32
    /**
33
     * Name of relation loader associated with.
34
     *
35
     * @var string
36
     */
37
    protected $relation;
38
39
    /**
40
     * Default set of relation options. Child implementation might defined their of default options.
41
     *
42
     * @var array
43
     */
44
    protected $options = [
45
        //Load method, see QueryLoader constants
46
        'method' => null,
47
48
        //When true all loader columns will be minified (only for loading)
49
        'minify' => true,
50
51
        //Table alias
52
        'alias'  => null,
53
54
        //Alias used by another relation
55
        'using'  => null,
56
57
        //Where conditions (if any)
58
        'where'  => null,
59
    ];
60
61
    /**
62
     * @param string       $class
63
     * @param string       $relation
64
     * @param array        $schema
65
     * @param ORMInterface $orm
66
     */
67
    public function __construct(string $class, string $relation, array $schema, ORMInterface $orm)
68
    {
69
        parent::__construct($class, $schema, $orm);
70
71
        //We need related model primary keys in order to ensure that
72
        $this->schema[Record::SH_PRIMARY_KEY] = $orm->define($class, ORMInterface::R_PRIMARY_KEY);
73
        $this->relation = $relation;
74
    }
75
76
    /**
77
     * {@inheritdoc}
78
     */
79
    public function withContext(LoaderInterface $parent, array $options = []): LoaderInterface
80
    {
81
        /**
82
         * @var AbstractLoader $parent
83
         * @var self           $loader
84
         */
85
        $loader = parent::withContext($parent, $options);
86
87
        if ($loader->getDatabase() != $parent->getDatabase()) {
88
            if ($loader->isJoined()) {
89
                throw new LoaderException('Unable to join tables located in different databases');
90
            }
91
92
            //Loader is not joined, let's make sure that POSTLOAD is used
93
            if ($this->isLoaded()) {
94
                $loader->options['method'] = self::POSTLOAD;
95
            }
96
        }
97
98
        //Calculate table alias
99
        $loader->ensureAlias($parent);
100
101
        return $loader;
102
    }
103
104
    /**
105
     * Indicated that loaded must generate JOIN statement.
106
     *
107
     * @return bool
108
     */
109
    public function isJoined(): bool
110
    {
111
        if (!empty($this->options['using'])) {
112
            return true;
113
        }
114
115
        return in_array($this->getMethod(), [self::INLOAD, self::JOIN, self::LEFT_JOIN]);
116
    }
117
118
    /**
119
     * Indication that loader want to load data.
120
     *
121
     * @return bool
122
     */
123
    public function isLoaded(): bool
124
    {
125
        return $this->getMethod() !== self::JOIN && $this->getMethod() !== self::LEFT_JOIN;
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131
    public function loadData(AbstractNode $node)
132
    {
133
        if ($this->isJoined() || !$this->isLoaded()) {
134
            //Loading data for all nested relations
135
            parent::loadData($node);
136
137
            return;
138
        }
139
140
        $references = $node->getReferences();
141
        if (empty($references)) {
142
            //Nothing found at parent level, unable to create sub query
143
            return;
144
        }
145
146
        //Ensure all nested relations
147
        $statement = $this->configureQuery($this->createQuery(), true, $references)->run();
148
        $statement->setFetchMode(\PDO::FETCH_NUM);
149
150
        foreach ($statement as $row) {
151
            $node->parseRow(0, $row);
152
        }
153
154
        $statement->close();
155
156
        //Loading data for all nested relations
157
        parent::loadData($node);
158
    }
159
160
    /**
161
     * Configure query with conditions, joins and columns.
162
     *
163
     * @param SelectQuery $query
164
     * @param bool        $loadColumns
165
     * @param array       $outerKeys Set of OUTER_KEY values collected by parent loader.
166
     *
167
     * @return SelectQuery
168
     */
169
    protected function configureQuery(SelectQuery $query, bool $loadColumns = true, array $outerKeys = []): SelectQuery
170
    {
171
        if ($loadColumns) {
172
            if ($this->isJoined()) {
173
                //Mounting columns
174
                $this->mountColumns($query, $this->options['minify']);
175
            } else {
176
                //This is initial set of columns (remove all existed)
177
                $this->mountColumns($query, $this->options['minify'], '', true);
178
            }
179
        }
180
181
        return parent::configureQuery($query);
182
    }
183
184
    /**
185
     * Relation table alias.
186
     *
187
     * @return string
188
     */
189
    protected function getAlias(): string
190
    {
191
        if (!empty($this->options['using'])) {
192
            //We are using another relation (presumably defined by with() to load data).
193
            return $this->options['using'];
194
        }
195
196
        if (!empty($this->options['alias'])) {
197
            return $this->options['alias'];
198
        }
199
200
        throw new LoaderException("Unable to resolve loader alias");
201
    }
202
203
    /**
204
     * Relation columns.
205
     *
206
     * @return array
207
     */
208
    protected function getColumns(): array
209
    {
210
        return $this->schema[Record::RELATION_COLUMNS];
211
    }
212
213
    /**
214
     * Get load method.
215
     *
216
     * @return int
217
     */
218
    protected function getMethod(): int
219
    {
220
        return $this->options['method'];
221
    }
222
223
    /**
224
     * Generate sql identifier using loader alias and value from relation definition. Key name to be
225
     * fetched from schema.
226
     *
227
     * Example:
228
     * $this->getKey(Record::OUTER_KEY);
229
     *
230
     * @param string $key
231
     *
232
     * @return string|null
233
     */
234
    protected function localKey($key)
235
    {
236
        if (empty($this->schema[$key])) {
237
            return null;
238
        }
239
240
        return $this->getAlias() . '.' . $this->schema[$key];
241
    }
242
243
    /**
244
     * Get parent identifier based on relation configuration key.
245
     *
246
     * @param $key
247
     *
248
     * @return string
249
     */
250
    protected function parentKey($key): string
251
    {
252
        return $this->parent->getAlias() . '.' . $this->schema[$key];
253
    }
254
255
    /**
256
     * Ensure table alias.
257
     *
258
     * @param AbstractLoader $parent
259
     */
260
    protected function ensureAlias(AbstractLoader $parent)
261
    {
262
        //Let's calculate loader alias
263
        if (empty($this->options['alias'])) {
264
            if ($this->isLoaded() && $this->isJoined()) {
265
                //Let's create unique alias, we are able to do that for relations just loaded
266
                $this->options['alias'] = 'd' . decoct(++self::$countLevels);
267
            } else {
268
                //Let's use parent alias to continue chain
269
                $this->options['alias'] = $parent->getAlias() . '_' . $this->relation;
270
            }
271
        }
272
    }
273
274
    /**
275
     * Create relation specific select query.
276
     *
277
     * @return SelectQuery
278
     */
279
    protected function createQuery(): SelectQuery
280
    {
281
        return $this->orm->table($this->class)->select()->from(
282
            "{$this->getTable()} AS {$this->getAlias()}"
283
        );
284
    }
285
}