/
RelationList.php
188 lines (169 loc) · 5.85 KB
/
RelationList.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
<?php
namespace SilverStripe\ORM;
use Exception;
use Sminnee\CallbackList\CallbackList;
use SilverStripe\ORM\DB;
/**
* A DataList that represents a relation.
*
* Adds the notion of a foreign ID that can be optionally set.
*
* @template T of DataObject
* @extends DataList<T>
* @implements Relation<T>
*/
abstract class RelationList extends DataList implements Relation
{
/**
* @var CallbackList|null
*/
protected $addCallbacks;
/**
* @var CallbackList|null
*/
protected $removeCallbacks;
/**
* Manage callbacks which are called after the add() action is completed.
* Each callback will be passed (RelationList $this, DataObject|int $item, array $extraFields).
* If a relation methods is manually defined, this can be called to adjust the behaviour
* when adding records to this list.
*
* Needs to be defined through an overloaded relationship getter
* to ensure it is set consistently. These getters return a new object
* every time they're called.
*
* Note that subclasses of RelationList must implement the callback for it to function
*/
public function addCallbacks(): CallbackList
{
if (!$this->addCallbacks) {
$this->addCallbacks = new CallbackList();
}
return $this->addCallbacks;
}
/**
* Manage callbacks which are called after the remove() action is completed.
* Each Callback will be passed (RelationList $this, [int] $removedIds).
*
* Needs to be defined through an overloaded relationship getter
* to ensure it is set consistently. These getters return a new object
* every time they're called. Example:
*
* ```php
* class MyObject extends DataObject()
* {
* private static $many_many = [
* 'MyRelationship' => '...',
* ];
* public function MyRelationship()
* {
* $list = $this->getManyManyComponents('MyRelationship');
* $list->removeCallbacks()->add(function ($removedIds) {
* // ...
* });
* return $list;
* }
* }
* ```
*
* If a relation methods is manually defined, this can be called to adjust the behaviour
* when adding records to this list.
*
* Subclasses of RelationList must implement the callback for it to function
*/
public function removeCallbacks(): CallbackList
{
if (!$this->removeCallbacks) {
$this->removeCallbacks = new CallbackList();
}
return $this->removeCallbacks;
}
/**
* Any number of foreign keys to apply to this list
*
* @return string|array|null
*/
public function getForeignID()
{
return $this->dataQuery->getQueryParam('Foreign.ID');
}
public function getQueryParams()
{
$params = parent::getQueryParams();
// Remove `Foreign.` query parameters for created objects,
// as this would interfere with relations on those objects.
foreach (array_keys($params ?? []) as $key) {
if (stripos($key ?? '', 'Foreign.') === 0) {
unset($params[$key]);
}
}
return $params;
}
/**
* Returns a copy of this list with the ManyMany relationship linked to
* the given foreign ID.
*
* @param int|array $id An ID or an array of IDs.
*
* @return static<T>
*/
public function forForeignID($id)
{
// Turn a 1-element array into a simple value
if (is_array($id) && sizeof($id ?? []) == 1) {
$id = reset($id);
}
// Calculate the new filter
$filter = $this->foreignIDFilter($id);
$list = $this->alterDataQuery(function (DataQuery $query) use ($id, $filter) {
// Check if there is an existing filter, remove if there is
$currentFilter = $query->getQueryParam('Foreign.Filter');
if ($currentFilter) {
try {
$query->removeFilterOn($currentFilter);
} catch (Exception $e) {
/* NOP */
}
}
// Add the new filter
$query->setQueryParam('Foreign.ID', $id);
$query->setQueryParam('Foreign.Filter', $filter);
$query->where($filter);
});
return $list;
}
/**
* Returns a where clause that filters the members of this relationship to
* just the related items.
*
*
* @param array|integer $id (optional) An ID or an array of IDs - if not provided, will use the current ids as
* per getForeignID
* @return array Condition In array(SQL => parameters format)
*/
abstract protected function foreignIDFilter($id = null);
/**
* Prepare an array of IDs for a 'WHERE IN` clause deciding if we should use placeholders
* Current rules are to use not use placeholders, unless:
* - SilverStripe\ORM\DataList.use_placeholders_for_integer_ids is set to false, or
* - Any of the IDs values being filtered are not integers or valid integer strings
*
* Putting IDs directly into a where clause instead of using placeholders was measured to be significantly
* faster when querying a large number of IDs e.g. over 1000
*/
protected function prepareForeignIDsForWhereInClause(array $ids): string
{
if ($this->config()->get('use_placeholders_for_integer_ids')) {
return DB::placeholders($ids);
}
// Validate that we're only using int ID's for the IDs
// We need to do this to protect against SQL injection
foreach ($ids as $id) {
if (!ctype_digit((string) $id) || $id != (int) $id) {
return DB::placeholders($ids);
}
}
// explicitly including space after comma to match the default for DB::placeholders
return implode(', ', $ids);
}
}