|
1
|
|
|
<?php |
|
2
|
|
|
namespace Darya\ORM\Relation; |
|
3
|
|
|
|
|
4
|
|
|
use Darya\ORM\Record; |
|
5
|
|
|
use Darya\ORM\Relation; |
|
6
|
|
|
|
|
7
|
|
|
/** |
|
8
|
|
|
* Darya's many-to-many entity relation. |
|
9
|
|
|
* |
|
10
|
|
|
* @author Chris Andrew <[email protected]> |
|
11
|
|
|
*/ |
|
12
|
|
|
class BelongsToMany extends Relation { |
|
13
|
|
|
|
|
14
|
|
|
/** |
|
15
|
|
|
* @var array |
|
16
|
|
|
*/ |
|
17
|
|
|
protected $associationConstraint = array(); |
|
18
|
|
|
|
|
19
|
|
|
/** |
|
20
|
|
|
* @var string Table name for "many-to-many" relations |
|
21
|
|
|
*/ |
|
22
|
|
|
protected $table; |
|
23
|
|
|
|
|
24
|
|
|
/** |
|
25
|
|
|
* Instantiate a new many-to-many relation. |
|
26
|
|
|
* |
|
27
|
|
|
* @param Relation $parent |
|
28
|
|
|
* @param string $target |
|
29
|
|
|
* @param string $foreignKey [optional] |
|
30
|
|
|
* @param string $localKey [optional] |
|
31
|
|
|
* @param string $table [optional] |
|
32
|
|
|
* @param array $constraint [optional] |
|
33
|
|
|
*/ |
|
34
|
|
|
public function __construct(Record $parent, $target, $foreignKey = null, $localKey = null, $table = null, array $constraint = array(), array $associationConstraint = array()) { |
|
35
|
|
|
$this->localKey = $localKey; |
|
36
|
|
|
parent::__construct($parent, $target, $foreignKey); |
|
37
|
|
|
|
|
38
|
|
|
$this->table = $table; |
|
39
|
|
|
$this->setDefaultTable(); |
|
40
|
|
|
$this->constrain($constraint); |
|
41
|
|
|
$this->constrainAssociation($associationConstraint); |
|
42
|
|
|
} |
|
43
|
|
|
|
|
44
|
|
|
/** |
|
45
|
|
|
* Retrieve the IDs of models that should be inserted into the relation |
|
46
|
|
|
* table, given models that are already related and models that should be |
|
47
|
|
|
* associated. |
|
48
|
|
|
* |
|
49
|
|
|
* Returns the difference of the IDs of each set of instances. |
|
50
|
|
|
* |
|
51
|
|
|
* @param array $old |
|
52
|
|
|
* @param array $new |
|
53
|
|
|
* @return array |
|
54
|
|
|
*/ |
|
55
|
|
|
protected static function insertIds($old, $new) { |
|
56
|
|
|
$oldIds = array(); |
|
57
|
|
|
$newIds = array(); |
|
58
|
|
|
|
|
59
|
|
|
foreach ($old as $instance) { |
|
60
|
|
|
$oldIds[] = $instance->id(); |
|
61
|
|
|
} |
|
62
|
|
|
|
|
63
|
|
|
foreach ($new as $instance) { |
|
64
|
|
|
$newIds[] = $instance->id(); |
|
65
|
|
|
} |
|
66
|
|
|
|
|
67
|
|
|
$insert = array_diff($newIds, $oldIds); |
|
68
|
|
|
|
|
69
|
|
|
return $insert; |
|
70
|
|
|
} |
|
71
|
|
|
|
|
72
|
|
|
/** |
|
73
|
|
|
* Group foreign keys into arrays for each local key found. |
|
74
|
|
|
* |
|
75
|
|
|
* Expects an array with at least local keys and foreign keys set. |
|
76
|
|
|
* |
|
77
|
|
|
* Returns an adjacency list of local keys to related foreign keys. |
|
78
|
|
|
* |
|
79
|
|
|
* @param array $relations |
|
80
|
|
|
* @return array |
|
81
|
|
|
*/ |
|
82
|
|
|
protected function bundleRelations(array $relations) { |
|
83
|
|
|
$bundle = array(); |
|
84
|
|
|
|
|
85
|
|
|
foreach ($relations as $relation) { |
|
86
|
|
|
if (!isset($bundle[$relation[$this->localKey]])) { |
|
87
|
|
|
$bundle[$relation[$this->localKey]] = array(); |
|
88
|
|
|
} |
|
89
|
|
|
|
|
90
|
|
|
$bundle[$relation[$this->localKey]][] = $relation[$this->foreignKey]; |
|
91
|
|
|
} |
|
92
|
|
|
|
|
93
|
|
|
return $bundle; |
|
94
|
|
|
} |
|
95
|
|
|
|
|
96
|
|
|
/** |
|
97
|
|
|
* List the given instances with their IDs as keys. |
|
98
|
|
|
* |
|
99
|
|
|
* @param Record[]|Record|array $instances |
|
100
|
|
|
* @return Record[] |
|
101
|
|
|
*/ |
|
102
|
|
|
protected static function listById($instances) { |
|
103
|
|
|
$list = array(); |
|
104
|
|
|
|
|
105
|
|
|
foreach ((array) $instances as $instance) { |
|
106
|
|
|
$list[$instance->id()] = $instance; |
|
107
|
|
|
} |
|
108
|
|
|
|
|
109
|
|
|
return $list; |
|
110
|
|
|
} |
|
111
|
|
|
|
|
112
|
|
|
/** |
|
113
|
|
|
* Set the default keys for the relation if they have not already been set. |
|
114
|
|
|
*/ |
|
115
|
|
View Code Duplication |
protected function setDefaultKeys() { |
|
|
|
|
|
|
116
|
|
|
if (!$this->foreignKey) { |
|
117
|
|
|
$this->foreignKey = $this->prepareForeignKey(get_class($this->target)); |
|
118
|
|
|
} |
|
119
|
|
|
|
|
120
|
|
|
if (!$this->localKey) { |
|
121
|
|
|
$this->localKey = $this->prepareForeignKey(get_class($this->parent)); |
|
122
|
|
|
} |
|
123
|
|
|
} |
|
124
|
|
|
|
|
125
|
|
|
/** |
|
126
|
|
|
* Set the default many-to-many relation table name. |
|
127
|
|
|
* |
|
128
|
|
|
* Sorts parent and related class names alphabetically. |
|
129
|
|
|
*/ |
|
130
|
|
|
protected function setDefaultTable() { |
|
131
|
|
|
if ($this->table) { |
|
132
|
|
|
return; |
|
133
|
|
|
} |
|
134
|
|
|
|
|
135
|
|
|
$parent = $this->delimitClass(get_class($this->parent)); |
|
136
|
|
|
$target = $this->delimitClass(get_class($this->target)); |
|
137
|
|
|
|
|
138
|
|
|
$names = array($parent, $target); |
|
139
|
|
|
sort($names); |
|
140
|
|
|
|
|
141
|
|
|
$this->table = implode('_', $names) . 's'; |
|
142
|
|
|
} |
|
143
|
|
|
|
|
144
|
|
|
/** |
|
145
|
|
|
* Retrieve the default filter for the association table. |
|
146
|
|
|
* |
|
147
|
|
|
* @return array |
|
148
|
|
|
*/ |
|
149
|
|
|
protected function defaultAssociationConstraint() { |
|
150
|
|
|
return array($this->localKey => $this->parent->id()); |
|
151
|
|
|
} |
|
152
|
|
|
|
|
153
|
|
|
/** |
|
154
|
|
|
* Set a filter to constrain the association table. |
|
155
|
|
|
* |
|
156
|
|
|
* @param array $filter |
|
157
|
|
|
*/ |
|
158
|
|
|
public function constrainAssociation(array $filter) { |
|
159
|
|
|
$this->associationConstraint = $filter; |
|
160
|
|
|
} |
|
161
|
|
|
|
|
162
|
|
|
/** |
|
163
|
|
|
* Retrieve the custom filter used to constrain the association table. |
|
164
|
|
|
* |
|
165
|
|
|
* @return array |
|
166
|
|
|
*/ |
|
167
|
|
|
public function associationConstraint() { |
|
168
|
|
|
return $this->associationConstraint; |
|
169
|
|
|
} |
|
170
|
|
|
|
|
171
|
|
|
/** |
|
172
|
|
|
* Retrieve the filter for the association table. |
|
173
|
|
|
* |
|
174
|
|
|
* @return array |
|
175
|
|
|
*/ |
|
176
|
|
|
public function associationFilter() { |
|
177
|
|
|
return array_merge($this->defaultAssociationConstraint(), $this->associationConstraint()); |
|
178
|
|
|
} |
|
179
|
|
|
|
|
180
|
|
|
/** |
|
181
|
|
|
* Retrieve the filter for the related models. |
|
182
|
|
|
* |
|
183
|
|
|
* Optionally accepts a list of related IDs to filter by. |
|
184
|
|
|
* |
|
185
|
|
|
* TODO: Test this without the if statement and just merging regardless |
|
186
|
|
|
* |
|
187
|
|
|
* @param array $related |
|
188
|
|
|
* @return array |
|
189
|
|
|
*/ |
|
190
|
|
|
public function filter(array $related = array()) { |
|
191
|
|
|
$filter = array(); |
|
192
|
|
|
|
|
193
|
|
|
if (!empty($related)) { |
|
194
|
|
|
$filter[$this->target->key()] = $related; |
|
195
|
|
|
} |
|
196
|
|
|
|
|
197
|
|
|
$filter = array_merge($filter, $this->constraint()); |
|
198
|
|
|
|
|
199
|
|
|
return $filter; |
|
200
|
|
|
} |
|
201
|
|
|
|
|
202
|
|
|
/** |
|
203
|
|
|
* Retrieve the table of the many-to-many relation. |
|
204
|
|
|
* |
|
205
|
|
|
* @return string |
|
206
|
|
|
*/ |
|
207
|
|
|
public function table() { |
|
208
|
|
|
return $this->table; |
|
209
|
|
|
} |
|
210
|
|
|
|
|
211
|
|
|
/** |
|
212
|
|
|
* Retrieve the related IDs from the association table. |
|
213
|
|
|
* |
|
214
|
|
|
* Takes into consideration the regular relation filter, if it's not empty, |
|
215
|
|
|
* and loads IDs from the target table accordingly. |
|
216
|
|
|
* |
|
217
|
|
|
* @param int $limit |
|
218
|
|
|
*/ |
|
219
|
|
|
protected function relatedIds($limit = 0) { |
|
220
|
|
|
$associations = $this->storage()->read($this->table, $this->associationFilter(), null, $limit); |
|
221
|
|
|
|
|
222
|
|
|
if (empty($this->filter())) { |
|
223
|
|
|
return static::attributeList($associations, $this->foreignKey); |
|
224
|
|
|
} |
|
225
|
|
|
|
|
226
|
|
|
$filter = $this->filter(static::attributeList($associations, $this->foreignKey)); |
|
227
|
|
|
|
|
228
|
|
|
$related = $this->storage()->listing($this->target->table(), $this->target->key(), $filter, null, $limit); |
|
229
|
|
|
|
|
230
|
|
|
return static::attributeList($related, $this->target->key()); |
|
231
|
|
|
} |
|
232
|
|
|
|
|
233
|
|
|
/** |
|
234
|
|
|
* Retrieve the data of the related models. |
|
235
|
|
|
* |
|
236
|
|
|
* @param int $limit |
|
237
|
|
|
* @return array |
|
238
|
|
|
*/ |
|
239
|
|
|
public function read($limit = 0) { |
|
240
|
|
|
return $this->storage()->read($this->target->table(), array( |
|
241
|
|
|
$this->target->key() => $this->relatedIds($limit) |
|
242
|
|
|
)); |
|
243
|
|
|
} |
|
244
|
|
|
|
|
245
|
|
|
/** |
|
246
|
|
|
* Eagerly load the related models for the given parent instances. |
|
247
|
|
|
* |
|
248
|
|
|
* Returns the given instances with their related models loaded. |
|
249
|
|
|
* |
|
250
|
|
|
* @param array $instances |
|
251
|
|
|
* @param string $name TODO: Remove this and store as a property |
|
252
|
|
|
* @return array |
|
253
|
|
|
*/ |
|
254
|
|
|
public function eager(array $instances, $name) { |
|
255
|
|
|
$this->verifyParents($instances); |
|
256
|
|
|
|
|
257
|
|
|
// Grab IDs of parent instances |
|
258
|
|
|
$ids = static::attributeList($instances, $this->parent->key()); |
|
259
|
|
|
|
|
260
|
|
|
// Build the filter for the association table |
|
261
|
|
|
$filter = array_merge($this->associationFilter(), array( |
|
262
|
|
|
$this->localKey => $ids |
|
263
|
|
|
)); |
|
264
|
|
|
|
|
265
|
|
|
// Read the relations from the table |
|
266
|
|
|
$relations = $this->storage()->read($this->table, $filter); |
|
267
|
|
|
|
|
268
|
|
|
// Unique list of target keys |
|
269
|
|
|
$relatedIds = static::attributeList($relations, $this->foreignKey); |
|
270
|
|
|
$relatedIds = array_unique($relatedIds); |
|
271
|
|
|
|
|
272
|
|
|
// Adjacency list of parent keys to target keys |
|
273
|
|
|
$relationBundle = $this->bundleRelations($relations); |
|
274
|
|
|
|
|
275
|
|
|
// Build the filter for the related models |
|
276
|
|
|
$filter = $this->filter($relatedIds); |
|
277
|
|
|
|
|
278
|
|
|
// Data of relations |
|
279
|
|
|
$data = $this->storage()->read($this->target->table(), $filter); |
|
280
|
|
|
|
|
281
|
|
|
// Instances of relations from the data |
|
282
|
|
|
$class = get_class($this->target); |
|
283
|
|
|
$generated = $class::generate($data); |
|
284
|
|
|
|
|
285
|
|
|
// Set IDs as the keys of the relation instances |
|
286
|
|
|
$list = static::listById($generated); |
|
287
|
|
|
|
|
288
|
|
|
// Attach the related instances using the relation adjacency list |
|
289
|
|
|
foreach ($instances as $instance) { |
|
290
|
|
|
$instanceRelations = array(); |
|
291
|
|
|
|
|
292
|
|
|
// TODO: Find a way to drop these issets |
|
293
|
|
|
if (isset($relationBundle[$instance->id()])) { |
|
294
|
|
|
foreach ($relationBundle[$instance->id()] as $relationId) { |
|
295
|
|
|
if (isset($list[$relationId])) { |
|
296
|
|
|
$instanceRelations[] = $list[$relationId]; |
|
297
|
|
|
} |
|
298
|
|
|
} |
|
299
|
|
|
} |
|
300
|
|
|
|
|
301
|
|
|
$instance->relation($name)->set($instanceRelations); |
|
302
|
|
|
} |
|
303
|
|
|
|
|
304
|
|
|
return $instances; |
|
305
|
|
|
} |
|
306
|
|
|
|
|
307
|
|
|
/** |
|
308
|
|
|
* Retrieve the related models. |
|
309
|
|
|
* |
|
310
|
|
|
* @return Record[] |
|
311
|
|
|
*/ |
|
312
|
|
|
public function retrieve() { |
|
313
|
|
|
return $this->all(); |
|
314
|
|
|
} |
|
315
|
|
|
|
|
316
|
|
|
/** |
|
317
|
|
|
* Associate the given models. |
|
318
|
|
|
* |
|
319
|
|
|
* Returns the number of models successfully associated. |
|
320
|
|
|
* |
|
321
|
|
|
* @param Record[]|Record $instances |
|
322
|
|
|
* @return int |
|
323
|
|
|
*/ |
|
324
|
|
|
public function associate($instances) { |
|
325
|
|
|
$instances = static::arrayify($instances); |
|
326
|
|
|
|
|
327
|
|
|
$existing = $this->storage()->read($this->table, array( |
|
328
|
|
|
$this->localKey => $this->parent->id() |
|
329
|
|
|
)); |
|
330
|
|
|
|
|
331
|
|
|
$successful = 0; |
|
332
|
|
|
|
|
333
|
|
|
foreach ($instances as $instance) { |
|
334
|
|
|
$this->verify($instance); |
|
335
|
|
|
|
|
336
|
|
|
if ($instance->save()) { |
|
337
|
|
|
$successful++; |
|
338
|
|
|
$this->replace($instance); |
|
339
|
|
|
|
|
340
|
|
|
if (!in_array($instance->id(), $existing)) { |
|
341
|
|
|
$this->storage()->create($this->table, array( |
|
342
|
|
|
$this->localKey => $this->parent->id(), |
|
343
|
|
|
$this->foreignKey => $instance->id() |
|
344
|
|
|
)); |
|
345
|
|
|
} |
|
346
|
|
|
}; |
|
347
|
|
|
} |
|
348
|
|
|
|
|
349
|
|
|
return $successful; |
|
350
|
|
|
} |
|
351
|
|
|
|
|
352
|
|
|
/** |
|
353
|
|
|
* Dissociate the given models from the parent model. |
|
354
|
|
|
* |
|
355
|
|
|
* Returns the number of models successfully dissociated. |
|
356
|
|
|
* |
|
357
|
|
|
* @param Record[]|Record $instances [optional] |
|
358
|
|
|
* @return int |
|
359
|
|
|
*/ |
|
360
|
|
|
public function dissociate($instances) { |
|
361
|
|
|
$instances = static::arrayify($instances); |
|
362
|
|
|
|
|
363
|
|
|
$ids = array(); |
|
364
|
|
|
|
|
365
|
|
|
$this->verify($instances); |
|
366
|
|
|
|
|
367
|
|
|
foreach ($instances as $instance) { |
|
368
|
|
|
$ids[] = $instance->id(); |
|
369
|
|
|
} |
|
370
|
|
|
|
|
371
|
|
|
$ids = array_intersect($ids, $this->relatedIds()); |
|
372
|
|
|
|
|
373
|
|
|
$successful = $this->storage()->delete($this->table, array_merge( |
|
374
|
|
|
$this->associationFilter(), |
|
375
|
|
|
array($this->foreignKey => $ids) |
|
376
|
|
|
)); |
|
377
|
|
|
|
|
378
|
|
|
$this->reduce($ids); |
|
379
|
|
|
|
|
380
|
|
|
return (int) $successful; |
|
381
|
|
|
} |
|
382
|
|
|
|
|
383
|
|
|
/** |
|
384
|
|
|
* Dissociate all currently associated models. |
|
385
|
|
|
* |
|
386
|
|
|
* Returns the number of models successfully dissociated. |
|
387
|
|
|
* |
|
388
|
|
|
* @return int |
|
389
|
|
|
*/ |
|
390
|
|
|
public function purge() { |
|
391
|
|
|
$this->related = null; // Force a reload because diffing would be a pain |
|
392
|
|
|
|
|
393
|
|
|
return (int) $this->storage()->delete($this->table, array( |
|
394
|
|
|
$this->foreignKey => $this->relatedIds() |
|
395
|
|
|
)); |
|
396
|
|
|
} |
|
397
|
|
|
|
|
398
|
|
|
/** |
|
399
|
|
|
* Dissociate all models and associate the given models. |
|
400
|
|
|
* |
|
401
|
|
|
* Returns the number of models successfully associated. |
|
402
|
|
|
* |
|
403
|
|
|
* @param Record[]|Record $instances [optional] |
|
404
|
|
|
* @return int |
|
405
|
|
|
*/ |
|
406
|
|
|
public function sync($instances) { |
|
407
|
|
|
$this->purge(); |
|
408
|
|
|
|
|
409
|
|
|
return $this->associate($instances); |
|
410
|
|
|
} |
|
411
|
|
|
|
|
412
|
|
|
/** |
|
413
|
|
|
* Count the number of related model instances. |
|
414
|
|
|
* |
|
415
|
|
|
* Counts loaded instances if they are present, queries storage otherwise. |
|
416
|
|
|
* |
|
417
|
|
|
* @return int |
|
418
|
|
|
*/ |
|
419
|
|
|
public function count() { |
|
420
|
|
|
if ($this->loaded()) { |
|
421
|
|
|
return parent::count(); |
|
422
|
|
|
} |
|
423
|
|
|
|
|
424
|
|
|
if (empty($this->filter())) { |
|
425
|
|
|
return $this->storage()->count($this->table, $this->associationFilter()); |
|
426
|
|
|
} |
|
427
|
|
|
|
|
428
|
|
|
$filter = $this->filter($this->relatedIds()); |
|
429
|
|
|
|
|
430
|
|
|
return $this->storage()->count($this->target->table(), $filter); |
|
431
|
|
|
} |
|
432
|
|
|
|
|
433
|
|
|
} |
|
434
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.