Completed
Push — master ( 3c05d4...72cf45 )
by Chris
50:08 queued 33:24
created

BelongsToMany::table()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 0
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() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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