Passed
Push — master ( 964b09...302996 )
by Daniel
59:27 queued 48:09
created

ManyManyThroughQueryManipulator::getForeignIDKey()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
4
namespace SilverStripe\ORM;
5
6
use SilverStripe\Core\Injector\Injectable;
7
use SilverStripe\Core\Injector\Injector;
8
use SilverStripe\Dev\Deprecation;
9
use SilverStripe\ORM\Queries\SQLSelect;
10
11
/**
12
 * Injected into DataQuery to augment getFinalisedQuery() with a join table
13
 */
14
class ManyManyThroughQueryManipulator implements DataQueryManipulator
15
{
16
17
    use Injectable;
18
19
    /**
20
     * DataObject that backs the joining table
21
     *
22
     * @var string
23
     */
24
    protected $joinClass;
25
26
    /**
27
     * Key that joins to the data class
28
     *
29
     * @var string $localKey
30
     */
31
    protected $localKey;
32
33
    /**
34
     * Key that joins to the parent class
35
     *
36
     * @var string $foreignKey
37
     */
38
    protected $foreignKey;
39
40
    /**
41
     * Foreign class 'from' property. Normally not needed unless polymorphic.
42
     *
43
     * @var string
44
     */
45
    protected $foreignClass;
46
47
    /**
48
     * Class name of instance that owns this list
49
     *
50
     * @var string
51
     */
52
    protected $parentClass;
53
54
    /**
55
     * Build query manipulator for a given join table. Additional parameters (foreign key, etc)
56
     * will be infered at evaluation from query parameters set via the ManyManyThroughList
57
     *
58
     * @param string $joinClass Class name of the joined dataobject record
59
     * @param string $localKey The key in the join table that maps to the dataClass' PK.
60
     * @param string $foreignKey The key in the join table that maps to joined class' PK.
61
     * @param string $foreignClass the 'from' class name
62
     * @param string $parentClass Name of parent class. Subclass of $foreignClass
63
     */
64
    public function __construct($joinClass, $localKey, $foreignKey, $foreignClass = null, $parentClass = null)
65
    {
66
        $this->setJoinClass($joinClass);
67
        $this->setLocalKey($localKey);
68
        $this->setForeignKey($foreignKey);
69
        if ($foreignClass) {
70
            $this->setForeignClass($foreignClass);
71
        } else {
72
            Deprecation::notice('5.0', 'Arg $foreignClass will be mandatory in 5.x');
73
        }
74
        if ($parentClass) {
75
            $this->setParentClass($parentClass);
76
        } else {
77
            Deprecation::notice('5.0', 'Arg $parentClass will be mandatory in 5.x');
78
        }
79
    }
80
81
    /**
82
     * @return string
83
     */
84
    public function getJoinClass()
85
    {
86
        return $this->joinClass;
87
    }
88
89
    /**
90
     * @param mixed $joinClass
91
     * @return $this
92
     */
93
    public function setJoinClass($joinClass)
94
    {
95
        $this->joinClass = $joinClass;
96
        return $this;
97
    }
98
99
    /**
100
     * @return string
101
     */
102
    public function getLocalKey()
103
    {
104
        return $this->localKey;
105
    }
106
107
    /**
108
     * @param string $localKey
109
     * @return $this
110
     */
111
    public function setLocalKey($localKey)
112
    {
113
        $this->localKey = $localKey;
114
        return $this;
115
    }
116
117
    /**
118
     * @return string
119
     */
120
    public function getForeignKey()
121
    {
122
        return $this->foreignKey;
123
    }
124
125
    /**
126
     * Gets ID key name for foreign key component
127
     *
128
     * @return string
129
     */
130
    public function getForeignIDKey()
131
    {
132
        $key = $this->getForeignKey();
133
        if ($this->getForeignClass() === DataObject::class) {
134
            return $key . 'ID';
135
        }
136
        return $key;
137
    }
138
139
    /**
140
     * Gets Class key name for foreign key component (or null if none)
141
     *
142
     * @return string|null
143
     */
144
    public function getForeignClassKey()
145
    {
146
        if ($this->getForeignClass() === DataObject::class) {
147
            return $this->getForeignKey() . 'Class';
148
        }
149
        return null;
150
    }
151
152
    /**
153
     * @param string $foreignKey
154
     * @return $this
155
     */
156
    public function setForeignKey($foreignKey)
157
    {
158
        $this->foreignKey = $foreignKey;
159
        return $this;
160
    }
161
162
    /**
163
     * Get has_many relationship between parent and join table (for a given DataQuery)
164
     *
165
     * @param DataQuery $query
166
     * @return HasManyList
167
     */
168
    public function getParentRelationship(DataQuery $query)
169
    {
170
        // Create has_many
171
        if ($this->getForeignClass() === DataObject::class) {
172
            /** @internal Polymorphic many_many is experimental */
173
            $list = PolymorphicHasManyList::create(
174
                $this->getJoinClass(),
0 ignored issues
show
Bug introduced by
$this->getJoinClass() of type string is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

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

174
                /** @scrutinizer ignore-type */ $this->getJoinClass(),
Loading history...
175
                $this->getForeignKey(),
176
                $this->getParentClass()
177
            );
178
        } else {
179
            $list = HasManyList::create($this->getJoinClass(), $this->getForeignKey());
180
        }
181
        $list = $list->setDataQueryParam($this->extractInheritableQueryParameters($query));
182
183
        // Limit to given foreign key
184
        $foreignID = $query->getQueryParam('Foreign.ID');
185
        if ($foreignID) {
186
            $list = $list->forForeignID($foreignID);
187
        }
188
        return $list;
189
    }
190
191
    /**
192
     * Calculate the query parameters that should be inherited from the base many_many
193
     * to the nested has_many list.
194
     *
195
     * @param DataQuery $query
196
     * @return mixed
197
     */
198
    public function extractInheritableQueryParameters(DataQuery $query)
199
    {
200
        $params = $query->getQueryParams();
201
202
        // Remove `Foreign.` query parameters for created objects,
203
        // as this would interfere with relations on those objects.
204
        foreach (array_keys($params) as $key) {
205
            if (stripos($key, 'Foreign.') === 0) {
206
                unset($params[$key]);
207
            }
208
        }
209
210
        // Get inheritable parameters from an instance of the base query dataclass
211
        $inst = Injector::inst()->create($query->dataClass());
212
        $inst->setSourceQueryParams($params);
213
        return $inst->getInheritableQueryParams();
214
    }
215
216
    /**
217
     * Get name of join table alias for use in queries.
218
     *
219
     * @return string
220
     */
221
    public function getJoinAlias()
222
    {
223
        return DataObject::getSchema()->tableName($this->getJoinClass());
224
    }
225
226
    /**
227
     * Invoked prior to getFinalisedQuery()
228
     *
229
     * @param DataQuery $dataQuery
230
     * @param array $queriedColumns
231
     * @param SQLSelect $sqlSelect
232
     */
233
    public function beforeGetFinalisedQuery(DataQuery $dataQuery, $queriedColumns = [], SQLSelect $sqlSelect)
234
    {
235
        // Get metadata and SQL from join table
236
        $hasManyRelation = $this->getParentRelationship($dataQuery);
237
        $joinTableSQLSelect = $hasManyRelation->dataQuery()->query();
238
        $joinTableSQL = $joinTableSQLSelect->sql($joinTableParameters);
239
        $joinTableColumns = array_keys($joinTableSQLSelect->getSelect()); // Get aliases (keys) only
240
        $joinTableAlias = $this->getJoinAlias();
241
242
        // Get fields to join on
243
        $localKey = $this->getLocalKey();
244
        $schema = DataObject::getSchema();
245
        $baseTable = $schema->baseDataClass($dataQuery->dataClass());
246
        $childField = $schema->sqlColumnForField($baseTable, 'ID');
247
248
        // Add select fields
249
        foreach ($joinTableColumns as $joinTableColumn) {
250
            $sqlSelect->selectField(
251
                "\"{$joinTableAlias}\".\"{$joinTableColumn}\"",
252
                "{$joinTableAlias}_{$joinTableColumn}"
253
            );
254
        }
255
256
        // Apply join and record sql for later insertion (at end of replacements)
257
        // By using a string placeholder $$_SUBQUERY_$$ we protect field/table rewrites from interfering twice
258
        // on the already-finalised inner list
259
        $sqlSelect->addInnerJoin(
260
            '(SELECT $$_SUBQUERY_$$)',
261
            "\"{$joinTableAlias}\".\"{$localKey}\" = {$childField}",
262
            $joinTableAlias,
263
            20,
264
            $joinTableParameters
265
        );
266
        $dataQuery->setQueryParam('Foreign.JoinTableSQL', $joinTableSQL);
267
268
        // After this join, and prior to afterGetFinalisedQuery, $sqlSelect will be populated with the
269
        // necessary sql rewrites (versioned, etc) that effect the base table.
270
        // By using a placeholder for the subquery we can protect the subquery (already rewritten)
271
        // from being re-written a second time. However we DO want the join predicate (above) to be rewritten.
272
        // See http://php.net/manual/en/function.str-replace.php#refsect1-function.str-replace-notes
273
        // for the reason we only add the final substitution at the end of getFinalisedQuery()
274
    }
275
276
    /**
277
     * Invoked after getFinalisedQuery()
278
     *
279
     * @param DataQuery $dataQuery
280
     * @param array $queriedColumns
281
     * @param SQLSelect $sqlQuery
282
     */
283
    public function afterGetFinalisedQuery(DataQuery $dataQuery, $queriedColumns = [], SQLSelect $sqlQuery)
284
    {
285
        // Inject final replacement after manipulation has been performed on the base dataquery
286
        $joinTableSQL = $dataQuery->getQueryParam('Foreign.JoinTableSQL');
287
        if ($joinTableSQL) {
288
            $sqlQuery->replaceText('SELECT $$_SUBQUERY_$$', $joinTableSQL);
289
            $dataQuery->setQueryParam('Foreign.JoinTableSQL', null);
290
        }
291
    }
292
293
    /**
294
     * @return string
295
     */
296
    public function getForeignClass()
297
    {
298
        return $this->foreignClass;
299
    }
300
301
    /**
302
     * @param string $foreignClass
303
     * @return $this
304
     */
305
    public function setForeignClass($foreignClass)
306
    {
307
        $this->foreignClass = $foreignClass;
308
        return $this;
309
    }
310
311
    /**
312
     * @return string
313
     */
314
    public function getParentClass()
315
    {
316
        return $this->parentClass;
317
    }
318
319
    /**
320
     * @param string $parentClass
321
     * @return ManyManyThroughQueryManipulator
322
     */
323
    public function setParentClass($parentClass)
324
    {
325
        $this->parentClass = $parentClass;
326
        return $this;
327
    }
328
}
329