Completed
Branch feature/pre-split (4c50c1)
by Anton
03:17
created

RelationLoader::withContext()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 24
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
271
     *
272
     * @return SelectQuery
273
     */
274
    protected function createQuery(): SelectQuery
275
    {
276
        return $this->orm->table($this->class)->select()->from(
277
            "{$this->getTable()} AS {$this->getAlias()}"
278
        );
279
    }
280
}