/
PolymorphicHasManyList.php
205 lines (179 loc) · 6.96 KB
/
PolymorphicHasManyList.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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
<?php
namespace SilverStripe\ORM;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Convert;
use InvalidArgumentException;
use SilverStripe\Dev\Deprecation;
use Traversable;
/**
* Represents a has_many list linked against a polymorphic relationship.
*
* @template T of DataObject
* @template TForeign of DataObject
* @extends HasManyList<T>
*/
class PolymorphicHasManyList extends HasManyList
{
/**
* Name of foreign key field that references the class name of the relation
*
* @var string
*/
protected $classForeignKey;
/**
* Name of the foreign key field that references the relation name, for has_one
* relations that can handle multiple reciprocal has_many relations.
*/
protected string $relationForeignKey;
/**
* Retrieve the name of the class this (has_many) relation is filtered by
*
* @return class-string<TForeign>
*/
public function getForeignClass()
{
return $this->dataQuery->getQueryParam('Foreign.Class');
}
/**
* Retrieve the name of the has_many relation this list is filtered by
*/
public function getForeignRelation(): ?string
{
return $this->dataQuery->getQueryParam('Foreign.Relation');
}
/**
* Retrieve the name of the has_many relation this list is filtered by
*
* @deprecated 5.2.0 Will be replaced with a parameter in the constructor
*/
public function setForeignRelation(string $relationName): static
{
Deprecation::notice('5.2.0', 'Will be replaced with a parameter in the constructor');
$foreignRelationColumn = DataObject::getSchema()->sqlColumnForField($this->dataClass, $this->relationForeignKey);
$this->dataQuery->where([$foreignRelationColumn => $relationName]);
$this->dataQuery->setQueryParam('Foreign.Relation', $relationName);
return $this;
}
/**
* Gets the field name which holds the related (has_many) object class.
*/
public function getForeignClassKey(): string
{
return $this->classForeignKey;
}
/**
* Gets the field name which holds the has_many relation name.
*
* Note that this will return a value even if the has_one relation
* doesn't support multiple reciprocal has_many relations.
*/
public function getForeignRelationKey(): string
{
return $this->relationForeignKey;
}
/**
* Create a new PolymorphicHasManyList relation list.
*
* @param class-string<T> $dataClass The class of the DataObjects that this will list.
* @param string $foreignField The name of the composite foreign (has_one) relation field. Used
* to generate the ID, Class, and Relation foreign keys.
* @param class-string<TForeign> $foreignClass Name of the class filter this relation is filtered against
*/
public function __construct($dataClass, $foreignField, $foreignClass)
{
// Set both id foreign key (as in HasManyList) and the class foreign key
parent::__construct($dataClass, "{$foreignField}ID");
$this->classForeignKey = "{$foreignField}Class";
$this->relationForeignKey = "{$foreignField}Relation";
// Ensure underlying DataQuery globally references the class filter
$this->dataQuery->setQueryParam('Foreign.Class', $foreignClass);
// For queries with multiple foreign IDs (such as that generated by
// DataList::relation) the filter must be generalised to filter by subclasses
$classNames = Convert::raw2sql(ClassInfo::subclassesFor($foreignClass));
$foreignClassColumn = DataObject::getSchema()->sqlColumnForField($dataClass, $this->classForeignKey);
$this->dataQuery->where(sprintf(
"$foreignClassColumn IN ('%s')",
implode("', '", $classNames)
));
}
public function add($item)
{
if (is_numeric($item)) {
$item = DataObject::get_by_id($this->dataClass, $item);
} elseif (!($item instanceof $this->dataClass)) {
throw new InvalidArgumentException(
"PolymorphicHasManyList::add() expecting a $this->dataClass object, or ID value"
);
}
$foreignID = $this->getForeignID();
// Validate foreignID
if (!$foreignID) {
user_error(
"PolymorphicHasManyList::add() can't be called until a foreign ID is set",
E_USER_WARNING
);
return;
}
if (is_array($foreignID)) {
user_error(
"PolymorphicHasManyList::add() can't be called on a list linked to multiple foreign IDs",
E_USER_WARNING
);
return;
}
// set the {$relationName}Class field value
$foreignKey = $this->foreignKey;
$classForeignKey = $this->classForeignKey;
$item->$foreignKey = $foreignID;
$item->$classForeignKey = $this->getForeignClass();
// set the {$relationName}Relation field value if appropriate
$foreignRelation = $this->getForeignRelation();
if ($foreignRelation) {
$relationForeignKey = $this->getForeignRelationKey();
$item->$relationForeignKey = $foreignRelation;
}
$item->write();
}
public function remove($item)
{
if (!($item instanceof $this->dataClass)) {
throw new InvalidArgumentException(
"HasManyList::remove() expecting a $this->dataClass object, or ID"
);
}
// Don't remove item with unrelated class key
$foreignClass = $this->getForeignClass();
$classNames = ClassInfo::subclassesFor($foreignClass);
$classForeignKey = $this->classForeignKey;
$classValueLower = strtolower($item->$classForeignKey ?? '');
if (!array_key_exists($classValueLower, $classNames ?? [])) {
return;
}
// Don't remove item with unrelated relation key
$foreignRelation = $this->getForeignRelation();
$relationForeignKey = $this->getForeignRelationKey();
if (!$this->relationMatches($item->$relationForeignKey, $foreignRelation)) {
return;
}
// Don't remove item which doesn't belong to this list
$foreignID = $this->getForeignID();
$foreignKey = $this->foreignKey;
if (empty($foreignID)
|| $foreignID == $item->$foreignKey
|| (is_array($foreignID) && in_array($item->$foreignKey, $foreignID ?? []))
) {
// Unset the foreign relation key if appropriate
if ($foreignRelation) {
$item->$relationForeignKey = null;
}
// Unset the rest of the relation and write the record
$item->$foreignKey = null;
$item->$classForeignKey = null;
$item->write();
}
}
private function relationMatches(?string $actual, ?string $expected): bool
{
return (empty($actual) && empty($expected)) || $actual === $expected;
}
}