Completed
Push — master ( ad0a2c...3d1a35 )
by Chris
02:24
created

BelongsToMany::defaultAssociationConstraint()   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
	 * @param array $old
50
	 * @param array $new
51
	 * @return array
52
	 */
53
	protected static function insertIds($old, $new) {
54
		$oldIds = array();
55
		$newIds = array();
56
		
57
		foreach ($old as $instance) {
58
			$oldIds[] = $instance->id();
59
		}
60
		
61
		foreach ($new as $instance) {
62
			$newIds[] = $instance->id();
63
		}
64
		
65
		$insert = array_diff($newIds, $oldIds);
66
		
67
		return $insert;
68
	}
69
	
70
	/**
71
	 * Group foreign keys into arrays for each local key found.
72
	 * 
73
	 * Expects an array with at least local keys and foreign keys set.
74
	 * 
75
	 * Returns an adjacency list of local keys to related foreign keys.
76
	 * 
77
	 * @param array $relations
78
	 * @return array
79
	 */
80
	protected function bundleRelations(array $relations) {
81
		$bundle = array();
82
		
83
		foreach ($relations as $relation) {
84
			if (!isset($bundle[$relation[$this->localKey]])) {
85
				$bundle[$relation[$this->localKey]] = array();
86
			}
87
			
88
			$bundle[$relation[$this->localKey]][] = $relation[$this->foreignKey];
89
		}
90
		
91
		return $bundle;
92
	}
93
	
94
	/**
95
	 * List the given instances with their IDs as keys.
96
	 * 
97
	 * @param Record[]|Record $instances
98
	 * @return Record[]
99
	 */
100
	protected static function listById($instances) {
101
		$list = array();
102
		
103
		foreach ((array) $instances as $instance) {
104
			$list[$instance->id()] = $instance;
105
		}
106
		
107
		return $list;
108
	}
109
	
110
	/**
111
	 * Set the default keys for the relation if they have not already been set.
112
	 */
113 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...
114
		if (!$this->foreignKey) {
115
			$this->foreignKey = $this->prepareForeignKey(get_class($this->target));
116
		}
117
		
118
		if (!$this->localKey) {
119
			$this->localKey = $this->prepareForeignKey(get_class($this->parent));
120
		}
121
	}
122
	
123
	/**
124
	 * Set the default many-to-many relation table name.
125
	 * 
126
	 * Sorts parent and related class names alphabetically.
127
	 */
128
	protected function setDefaultTable() {
129
		if ($this->table) {
130
			return;
131
		}
132
		
133
		$parent = $this->delimitClass(get_class($this->parent));
134
		$target = $this->delimitClass(get_class($this->target));
135
		
136
		$names = array($parent, $target);
137
		sort($names);
138
		
139
		$this->table = implode('_', $names) . 's';
140
	}
141
	
142
	/**
143
	 * Retrieve the default filter for the association table.
144
	 * 
145
	 * @return array
146
	 */
147
	protected function defaultAssociationConstraint() {
148
		return array($this->localKey => $this->parent->id());
149
	}
150
	
151
	/**
152
	 * Set a filter to constrain the association table.
153
	 * 
154
	 * @param array $filter
155
	 */
156
	public function constrainAssociation(array $filter) {
157
		$this->associationConstraint = $filter;
158
	}
159
	
160
	/**
161
	 * Retrieve the custom filter used to constrain the association table.
162
	 * 
163
	 * @return array
164
	 */
165
	public function associationConstraint() {
166
		return $this->associationConstraint;
167
	}
168
	
169
	/**
170
	 * Retrieve the filter for the association table.
171
	 * 
172
	 * @return array
173
	 */
174
	public function associationFilter() {
175
		return array_merge($this->defaultAssociationConstraint(), $this->associationConstraint());
176
	}
177
	
178
	/**
179
	 * Retrieve the filter for the related models.
180
	 * 
181
	 * Optionally accepts a list of related IDs to filter by.
182
	 * 
183
	 * TODO: Test this without the if statement and just merging regardless
184
	 * 
185
	 * @param array $related
186
	 * @return array
187
	 */
188
	public function filter(array $related = array()) {
189
		$filter = array();
190
		
191
		if (!empty($related)) {
192
			$filter[$this->target->key()] = $related;
193
		}
194
		
195
		$filter = array_merge($filter, $this->constraint());
196
		
197
		return $filter;
198
	}
199
	
200
	/**
201
	 * Retrieve the table of the many-to-many relation.
202
	 * 
203
	 * @return string
204
	 */
205
	public function table() {
206
		return $this->table;
207
	}
208
	
209
	/**
210
	 * Retrieve the related IDs from the association table.
211
	 * 
212
	 * @param int $limit
213
	 */
214
	protected function readAssociation($limit = 0) {
215
		$relations = $this->storage()->read($this->table, $this->associationFilter(), null, $limit);
216
		
217
		return static::attributeList($relations, $this->foreignKey);
218
	}
219
	
220
	/**
221
	 * Retrieve the data of the related models.
222
	 * 
223
	 * @param int $limit
224
	 * @return array
225
	 */
226
	public function read($limit = 0) {
227
		$related = $this->readAssociation($limit);
228
		
229
		$filter = $this->filter($related);
230
		
231
		return $this->storage()->read($this->target->table(), $filter);
232
	}
233
	
234
	/**
235
	 * Eagerly load the related models for the given parent instances.
236
	 * 
237
	 * Returns the given instances with their related models loaded.
238
	 * 
239
	 * @param array $instances
240
	 * @param string $name TODO: Remove this and store as a property
241
	 * @return array
242
	 */
243
	public function eager(array $instances, $name) {
244
		$this->verifyParents($instances);
245
		
246
		// Grab IDs of parent instances
247
		$ids = static::attributeList($instances, $this->parent->key());
248
		
249
		// Build the filter for the association table
250
		$filter = array_merge($this->associationFilter(), array(
251
			$this->localKey => $ids
252
		));
253
		
254
		// Read the relations from the table
255
		$relations = $this->storage()->read($this->table, $filter);
256
		
257
		// Unique list of target keys
258
		$relatedIds = static::attributeList($relations, $this->foreignKey);
259
		$relatedIds = array_unique($relatedIds);
260
		
261
		// Adjacency list of parent keys to target keys
262
		$relationBundle = $this->bundleRelations($relations);
263
		
264
		// Build the filter for the related models
265
		$filter = $this->filter($relatedIds);
266
		
267
		// Data of relations
268
		$data = $this->storage()->read($this->target->table(), $filter);
269
		
270
		// Instances of relations from the data
271
		$class = get_class($this->target);
272
		$generated = $class::generate($data);
273
		
274
		// Set IDs as the keys of the relation instances
275
		$list = static::listById($generated);
276
		
277
		// Attach the related instances using the relation adjacency list
278
		foreach ($instances as $instance) {
279
			$instanceRelations = array();
280
			
281
			// TODO: Find a way to drop these issets
282
			if (isset($relationBundle[$instance->id()])) {
283
				foreach ($relationBundle[$instance->id()] as $relationId) {
284
					if (isset($list[$relationId])) {
285
						$instanceRelations[] = $list[$relationId];
286
					}
287
				}
288
			}
289
			
290
			$instance->relation($name)->set($instanceRelations);
291
		}
292
		
293
		return $instances;
294
	}
295
	
296
	/**
297
	 * Retrieve the related models.
298
	 * 
299
	 * @return Record[]
300
	 */
301
	public function retrieve() {
302
		return $this->all();
303
	}
304
	
305
	/**
306
	 * Associate the given models.
307
	 * 
308
	 * Returns the number of models successfully associated.
309
	 * 
310
	 * @param Record[]|Record $instances
311
	 * @return int
312
	 */
313
	public function associate($instances) {
314
		$instances = static::arrayify($instances);
315
		
316
		$existing = $this->storage()->read($this->table, array(
317
			$this->localKey => $this->parent->id()
318
		));
319
		
320
		$successful = 0;
321
		
322
		foreach ($instances as $instance) {
323
			$this->verify($instance);
324
			
325
			if ($instance->save()) {
326
				$successful++;
327
				$this->replace($instance);
328
				
329
				if (!in_array($instance->id(), $existing)) {
330
					$this->storage()->create($this->table, array(
331
						$this->localKey   => $this->parent->id(),
332
						$this->foreignKey => $instance->id()
333
					));
334
				}
335
			};
336
		}
337
		
338
		return $successful;
339
	}
340
	
341
	/**
342
	 * Dissociate the given models.
343
	 * 
344
	 * Returns the number of models successfully dissociated.
345
	 * 
346
	 * @param Record[]|Record $instances [optional]
347
	 * @return int
348
	 */
349
	public function dissociate($instances) {
350
		$instances = static::arrayify($instances);
351
		
352
		$ids = array();
353
		
354
		$this->verify($instances);
355
		
356
		foreach ($instances as $instance) {
357
			$ids[] = $instance->id();
358
		}
359
		
360
		$successful = $this->storage()->delete($this->table, array_merge(
361
			$this->associationFilter(),
362
			array($this->foreignKey => $ids)
363
		));
364
		
365
		$this->reduce($ids);
366
		
367
		return (int) $successful;
368
	}
369
	
370
	/**
371
	 * Dissociate all currently associated models.
372
	 * 
373
	 * Returns the number of models successfully dissociated.
374
	 * 
375
	 * @return int
376
	 */
377
	public function purge() {
378
		$this->related = array();
379
		
380
		return (int) $this->storage()->delete($this->table, $this->associationFilter());
381
	}
382
	
383
	/**
384
	 * Dissociate all models and associate the given models.
385
	 * 
386
	 * Returns the number of models successfully associated.
387
	 * 
388
	 * @param Record[]|Record $instances [optional]
389
	 * @return int
390
	 */
391
	public function sync($instances) {
392
		$this->purge();
393
		
394
		return $this->associate($instances);
395
	}
396
	
397
	/**
398
	 * Count the number of related model instances.
399
	 * 
400
	 * Counts loaded instances if they are present, queries storage otherwise.
401
	 * 
402
	 * @return int
403
	 */
404
	public function count() {
405
		if ($this->loaded()) {
406
			return parent::count();
407
		}
408
		
409
		if (empty($this->filter())) {
410
			return $this->storage()->count($this->table, $this->associationFilter());
411
		}
412
		
413
		$filter = $this->filter($this->readAssociation());
414
		
415
		return $this->storage()->count($this->target->table(), $filter);
416
	}
417
	
418
}
419