BelongsToMany::listById()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Darya\ORM\Relation;
4
5
use Darya\ORM\Record;
6
use Darya\ORM\Relation;
7
use Exception;
8
9
/**
10
 * Darya's many-to-many entity relation.
11
 *
12
 * @property-read array  $associationConstraint
13
 * @property-read string $table
14
 *
15
 * @author Chris Andrew <[email protected]>
16
 */
17
class BelongsToMany extends Relation
18
{
19
	/**
20
	 * @var array
21
	 */
22
	protected $associationConstraint = [];
23
24
	/**
25
	 * @var string Table name for "many-to-many" relations
26
	 */
27
	protected $table;
28
29
	/**
30
	 * Instantiate a new many-to-many relation.
31
	 *
32
	 * @param Record $parent
33
	 * @param string $target
34
	 * @param string $foreignKey            [optional]
35
	 * @param string $localKey              [optional]
36
	 * @param string $table                 [optional]
37
	 * @param array  $constraint            [optional]
38
	 * @param array  $associationConstraint [optional]
39
	 */
40
	public function __construct(
41
		Record $parent,
42
		$target,
43
		$foreignKey = null,
44
		$localKey = null,
45
		$table = null,
46
		array $constraint = [],
47
		array $associationConstraint = []
48
	) {
49
		$this->localKey = $localKey;
0 ignored issues
show
Bug introduced by
The property localKey is declared read-only in Darya\ORM\Relation.
Loading history...
50
		parent::__construct($parent, $target, $foreignKey);
51
52
		$this->table = $table;
0 ignored issues
show
Bug introduced by
The property table is declared read-only in Darya\ORM\Relation\BelongsToMany.
Loading history...
53
		$this->setDefaultTable();
54
		$this->constrain($constraint);
55
		$this->constrainAssociation($associationConstraint);
56
	}
57
58
	/**
59
	 * Retrieve the IDs of models that should be inserted into the relation
60
	 * table, given models that are already related and models that should be
61
	 * associated.
62
	 *
63
	 * Returns the difference of the IDs of each set of instances.
64
	 *
65
	 * @param array $old
66
	 * @param array $new
67
	 * @return array
68
	 */
69
	protected static function insertIds($old, $new)
70
	{
71
		$oldIds = [];
72
		$newIds = [];
73
74
		foreach ($old as $instance) {
75
			$oldIds[] = $instance->id();
76
		}
77
78
		foreach ($new as $instance) {
79
			$newIds[] = $instance->id();
80
		}
81
82
		$insert = array_diff($newIds, $oldIds);
83
84
		return $insert;
85
	}
86
87
	/**
88
	 * Group foreign keys into arrays for each local key found.
89
	 *
90
	 * Expects an array with at least local keys and foreign keys set.
91
	 *
92
	 * Returns an adjacency list of local keys to related foreign keys.
93
	 *
94
	 * @param array $relations
95
	 * @return array
96
	 */
97
	protected function bundleRelations(array $relations)
98
	{
99
		$bundle = [];
100
101
		foreach ($relations as $relation) {
102
			if (!isset($bundle[$relation[$this->localKey]])) {
103
				$bundle[$relation[$this->localKey]] = [];
104
			}
105
106
			$bundle[$relation[$this->localKey]][] = $relation[$this->foreignKey];
107
		}
108
109
		return $bundle;
110
	}
111
112
	/**
113
	 * List the given instances with their IDs as keys.
114
	 *
115
	 * @param Record[]|Record|array $instances
116
	 * @return Record[]
117
	 */
118
	protected static function listById($instances)
119
	{
120
		$list = [];
121
122
		foreach ((array) $instances as $instance) {
123
			$list[$instance->id()] = $instance;
124
		}
125
126
		return $list;
127
	}
128
129
	/**
130
	 * Set the default keys for the relation if they have not already been set.
131
	 */
132
	protected function setDefaultKeys()
133
	{
134
		if (!$this->foreignKey) {
135
			$this->foreignKey = $this->prepareForeignKey(get_class($this->target));
0 ignored issues
show
Bug introduced by
The property foreignKey is declared read-only in Darya\ORM\Relation.
Loading history...
136
		}
137
138
		if (!$this->localKey) {
139
			$this->localKey = $this->prepareForeignKey(get_class($this->parent));
0 ignored issues
show
Bug introduced by
The property localKey is declared read-only in Darya\ORM\Relation.
Loading history...
140
		}
141
	}
142
143
	/**
144
	 * Set the default many-to-many relation table name.
145
	 *
146
	 * Sorts parent and related class names alphabetically.
147
	 */
148
	protected function setDefaultTable()
149
	{
150
		if ($this->table) {
151
			return;
152
		}
153
154
		$parent = $this->delimitClass(get_class($this->parent));
155
		$target = $this->delimitClass(get_class($this->target));
156
157
		$names = [$parent, $target];
158
		sort($names);
159
160
		$this->table = implode('_', $names) . 's';
0 ignored issues
show
Bug introduced by
The property table is declared read-only in Darya\ORM\Relation\BelongsToMany.
Loading history...
161
	}
162
163
	/**
164
	 * Retrieve the default filter for the association table.
165
	 *
166
	 * @return array
167
	 */
168
	protected function defaultAssociationConstraint()
169
	{
170
		return [$this->localKey => $this->parent->id()];
171
	}
172
173
	/**
174
	 * Set a filter to constrain the association table.
175
	 *
176
	 * @param array $filter
177
	 */
178
	public function constrainAssociation(array $filter)
179
	{
180
		$this->associationConstraint = $filter;
0 ignored issues
show
Bug introduced by
The property associationConstraint is declared read-only in Darya\ORM\Relation\BelongsToMany.
Loading history...
181
	}
182
183
	/**
184
	 * Retrieve the custom filter used to constrain the association table.
185
	 *
186
	 * @return array
187
	 */
188
	public function associationConstraint()
189
	{
190
		return $this->associationConstraint;
191
	}
192
193
	/**
194
	 * Retrieve the filter for the association table.
195
	 *
196
	 * @return array
197
	 */
198
	public function associationFilter()
199
	{
200
		return array_merge($this->defaultAssociationConstraint(), $this->associationConstraint());
201
	}
202
203
	/**
204
	 * Retrieve the filter for the related models.
205
	 *
206
	 * Optionally accepts a list of related IDs to filter by.
207
	 *
208
	 * @param array $related
209
	 * @return array
210
	 */
211
	public function filter(array $related = [])
212
	{
213
		$filter = [];
214
215
		// First filter by the currently related IDs if none are given
216
		$filter[$this->target->key()] = empty($related) ? $this->relatedIds() : $related;
217
218
		// Also filter by constraints
219
		$filter = array_merge($filter, $this->constraint());
220
221
		return $filter;
222
	}
223
224
	/**
225
	 * Retrieve and optionally set the table of the many-to-many relation.
226
	 *
227
	 * @param string $table [optional]
228
	 * @return string
229
	 */
230
	public function table($table = null)
231
	{
232
		$this->table = (string) $table ?: $this->table;
0 ignored issues
show
Bug introduced by
The property table is declared read-only in Darya\ORM\Relation\BelongsToMany.
Loading history...
233
234
		return $this->table;
235
	}
236
237
	/**
238
	 * Retrieve the related IDs from the association table.
239
	 *
240
	 * Takes into consideration the regular relation filter, if it's not empty,
241
	 * and loads IDs from the target table accordingly.
242
	 *
243
	 * @param int $limit
244
	 * @return int[]
245
	 */
246
	protected function relatedIds($limit = 0)
247
	{
248
		// Read the associations from the relation table
249
		$associations  = $this->storage()->read($this->table, $this->associationFilter(), null, $limit);
0 ignored issues
show
Bug introduced by
The method read() does not exist on Darya\Storage\Queryable. It seems like you code against a sub-type of said class. However, the method does not exist in Darya\ORM\EntityManager. Are you sure you never get one of those? ( Ignorable by Annotation )

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

249
		$associations  = $this->storage()->/** @scrutinizer ignore-call */ read($this->table, $this->associationFilter(), null, $limit);
Loading history...
250
		$associatedIds = static::attributeList($associations, $this->foreignKey);
251
252
		// If there's no constraint for the target table then we're done
253
		if (empty($this->constraint())) {
254
			return $associatedIds;
255
		}
256
257
		// Create the filter for the target table
258
		$filter = [];
259
260
		$filter[$this->target->key()] = $associatedIds;
261
262
		$filter = array_merge($filter, $this->constraint());
263
264
		// Load the matching related IDs from the target table
265
		$related = $this->storage()->listing($this->target->table(), $this->target->key(), $filter, null, $limit);
0 ignored issues
show
Bug introduced by
The method listing() does not exist on Darya\Storage\Queryable. It seems like you code against a sub-type of said class. However, the method does not exist in Darya\ORM\EntityManager. Are you sure you never get one of those? ( Ignorable by Annotation )

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

265
		$related = $this->storage()->/** @scrutinizer ignore-call */ listing($this->target->table(), $this->target->key(), $filter, null, $limit);
Loading history...
266
267
		return static::attributeList($related, $this->target->key());
268
	}
269
270
	/**
271
	 * Retrieve the data of the related models.
272
	 *
273
	 * @param int $limit
274
	 * @return array
275
	 */
276
	public function read($limit = 0)
277
	{
278
		return $this->storage()->read(
279
			$this->target->table(),
280
			[
281
				$this->target->key() => $this->relatedIds($limit)
282
			],
283
			$this->order()
284
		);
285
	}
286
287
	/**
288
	 * Eagerly load the related models for the given parent instances.
289
	 *
290
	 * Returns the given instances with their related models loaded.
291
	 *
292
	 * @param array $instances
293
	 * @return array
294
	 * @throws Exception
295
	 */
296
	public function eager(array $instances)
297
	{
298
		$this->verifyParents($instances);
299
300
		// Grab IDs of parent instances
301
		$ids = static::attributeList($instances, $this->parent->key());
302
303
		// Build the filter for the association table
304
		$filter = array_merge($this->associationFilter(), [
305
			$this->localKey => $ids
306
		]);
307
308
		// Read the relations from the table
309
		$relations = $this->storage()->read($this->table, $filter);
310
311
		// Unique list of target keys
312
		$relatedIds = static::attributeList($relations, $this->foreignKey);
313
		$relatedIds = array_unique($relatedIds);
314
315
		// Adjacency list of parent keys to target keys
316
		$relationBundle = $this->bundleRelations($relations);
317
318
		// Build the filter for the related models
319
		$filter = $this->filter($relatedIds);
320
321
		// Data of relations
322
		$data = $this->storage()->read($this->target->table(), $filter, $this->order());
323
324
		// Instances of relations from the data
325
		$class     = get_class($this->target);
326
		$generated = $class::generate($data);
327
328
		// Set IDs as the keys of the relation instances
329
		$list = static::listById($generated);
330
331
		// Attach the related instances using the relation adjacency list
332
		foreach ($instances as $instance) {
333
			$instanceRelations = [];
334
335
			// TODO: Find a way to drop these issets
336
			if (isset($relationBundle[$instance->id()])) {
337
				foreach ($relationBundle[$instance->id()] as $relationId) {
338
					if (isset($list[$relationId])) {
339
						$instanceRelations[] = $list[$relationId];
340
					}
341
				}
342
			}
343
344
			$instance->relation($this->name)->set($instanceRelations);
345
		}
346
347
		return $instances;
348
	}
349
350
	/**
351
	 * Retrieve the related models.
352
	 *
353
	 * @return Record[]
354
	 */
355
	public function retrieve()
356
	{
357
		return $this->all();
358
	}
359
360
	/**
361
	 * Associate the given models.
362
	 *
363
	 * Returns the number of models successfully associated.
364
	 *
365
	 * @param Record[]|Record $instances
366
	 * @return int
367
	 * @throws Exception
368
	 */
369
	public function associate($instances)
370
	{
371
		$this->verify($instances);
372
373
		$this->load();
374
375
		$this->attach($instances);
376
377
		$existing = $this->storage()->distinct($this->table, $this->foreignKey, [
0 ignored issues
show
Bug introduced by
The method distinct() does not exist on Darya\Storage\Queryable. It seems like you code against a sub-type of said class. However, the method does not exist in Darya\ORM\EntityManager. Are you sure you never get one of those? ( Ignorable by Annotation )

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

377
		$existing = $this->storage()->/** @scrutinizer ignore-call */ distinct($this->table, $this->foreignKey, [
Loading history...
378
			$this->localKey => $this->parent->id()
379
		]);
380
381
		$successful = 0;
382
383
		foreach ($this->related as $instance) {
384
			if ($instance->save()) {
385
				$successful++;
386
				$this->replace($instance);
387
388
				// Create the association in the relation table if it doesn't
389
				// yet exist
390
				if (!in_array($instance->id(), $existing)) {
391
					$this->storage()->create($this->table, [
0 ignored issues
show
Bug introduced by
The method create() does not exist on Darya\Storage\Queryable. It seems like you code against a sub-type of said class. However, the method does not exist in Darya\ORM\EntityManager. Are you sure you never get one of those? ( Ignorable by Annotation )

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

391
					$this->storage()->/** @scrutinizer ignore-call */ create($this->table, [
Loading history...
392
						$this->localKey   => $this->parent->id(),
393
						$this->foreignKey => $instance->id()
394
					]);
395
				}
396
			};
397
		}
398
399
		return $successful;
400
	}
401
402
	/**
403
	 * Dissociate the given models from the parent model.
404
	 *
405
	 * Returns the number of models successfully dissociated.
406
	 *
407
	 * @param Record[]|Record $instances [optional]
408
	 * @return int
409
	 * @throws Exception
410
	 */
411
	public function dissociate($instances = [])
412
	{
413
		$instances = static::arrayify($instances);
414
415
		$ids = [];
416
417
		$this->verify($instances);
418
419
		foreach ($instances as $instance) {
420
			$ids[] = $instance->id();
421
		}
422
423
		$ids = array_intersect($ids, $this->relatedIds());
424
425
		$successful = $this->storage()->delete($this->table, array_merge(
0 ignored issues
show
Bug introduced by
The method delete() does not exist on Darya\Storage\Queryable. It seems like you code against a sub-type of said class. However, the method does not exist in Darya\ORM\EntityManager. Are you sure you never get one of those? ( Ignorable by Annotation )

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

425
		$successful = $this->storage()->/** @scrutinizer ignore-call */ delete($this->table, array_merge(
Loading history...
426
			$this->associationFilter(),
427
			[$this->foreignKey => $ids]
428
		));
429
430
		$this->reduce($ids);
431
432
		return (int) $successful;
433
	}
434
435
	/**
436
	 * Dissociate all currently associated models.
437
	 *
438
	 * Returns the number of models successfully dissociated.
439
	 *
440
	 * @return int
441
	 */
442
	public function purge()
443
	{
444
		$this->clear(); // Force a reload because diffing would be a pain
445
446
		return (int) $this->storage()->delete($this->table, [
447
			$this->foreignKey => $this->relatedIds()
448
		]);
449
	}
450
451
	/**
452
	 * Dissociate all models and associate the given models.
453
	 *
454
	 * Returns the number of models successfully associated.
455
	 *
456
	 * @param Record[]|Record $instances [optional]
457
	 * @return int
458
	 * @throws Exception
459
	 */
460
	public function sync($instances)
461
	{
462
		$this->purge();
463
464
		return $this->associate($instances);
465
	}
466
467
	/**
468
	 * Count the number of related model instances.
469
	 *
470
	 * Counts loaded instances if they are present, queries storage otherwise.
471
	 *
472
	 * @return int
473
	 */
474
	public function count()
475
	{
476
		if (!$this->loaded() && empty($this->related)) {
477
			if (empty($this->filter())) {
478
				return $this->storage()->count($this->table, $this->associationFilter());
0 ignored issues
show
Bug introduced by
The method count() does not exist on Darya\Storage\Queryable. It seems like you code against a sub-type of said class. However, the method does not exist in Darya\ORM\EntityManager. Are you sure you never get one of those? ( Ignorable by Annotation )

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

478
				return $this->storage()->/** @scrutinizer ignore-call */ count($this->table, $this->associationFilter());
Loading history...
479
			}
480
481
			$filter = $this->filter($this->relatedIds());
482
483
			return $this->storage()->count($this->target->table(), $filter);
484
		}
485
486
		return parent::count();
487
	}
488
}
489