1: <?php namespace Laravel\Database\Eloquent\Relationships;
2:
3: use Laravel\Str;
4: use Laravel\Database\Eloquent\Model;
5: use Laravel\Database\Eloquent\Pivot;
6:
7: class Has_Many_And_Belongs_To extends Relationship {
8:
9: /**
10: * The name of the intermediate, joining table.
11: *
12: * @var string
13: */
14: protected $joining;
15:
16: /**
17: * The other or "associated" key. This is the foreign key of the related model.
18: *
19: * @var string
20: */
21: protected $other;
22:
23: /**
24: * The columns on the joining table that should be fetched.
25: *
26: * @var array
27: */
28: protected $with = array('id');
29:
30: /**
31: * Create a new many to many relationship instance.
32: *
33: * @param Model $model
34: * @param string $associated
35: * @param string $table
36: * @param string $foreign
37: * @param string $other
38: * @return void
39: */
40: public function __construct($model, $associated, $table, $foreign, $other)
41: {
42: $this->other = $other;
43:
44: $this->joining = $table ?: $this->joining($model, $associated);
45:
46: // If the Pivot table is timestamped, we'll set the timestamp columns to be
47: // fetched when the pivot table models are fetched by the developer else
48: // the ID will be the only "extra" column fetched in by default.
49: if (Pivot::$timestamps)
50: {
51: $this->with[] = 'created_at';
52:
53: $this->with[] = 'updated_at';
54: }
55:
56: parent::__construct($model, $associated, $foreign);
57: }
58:
59: /**
60: * Determine the joining table name for the relationship.
61: *
62: * By default, the name is the models sorted and joined with underscores.
63: *
64: * @return string
65: */
66: protected function joining($model, $associated)
67: {
68: $models = array(class_basename($model), class_basename($associated));
69:
70: sort($models);
71:
72: return strtolower($models[0].'_'.$models[1]);
73: }
74:
75: /**
76: * Get the properly hydrated results for the relationship.
77: *
78: * @return array
79: */
80: public function results()
81: {
82: return parent::get();
83: }
84:
85: /**
86: * Insert a new record into the joining table of the association.
87: *
88: * @param Model|int $id
89: * @param array $attributes
90: * @return bool
91: */
92: public function attach($id, $attributes = array())
93: {
94: if ($id instanceof Model) $id = $id->get_key();
95:
96: $joining = array_merge($this->join_record($id), $attributes);
97:
98: return $this->insert_joining($joining);
99: }
100:
101: /**
102: * Detach a record from the joining table of the association.
103: *
104: * @param array|Model|int $ids
105: * @return bool
106: */
107: public function detach($ids)
108: {
109: if ($ids instanceof Model) $ids = array($ids->get_key());
110: elseif ( ! is_array($ids)) $ids = array($ids);
111:
112: return $this->pivot()->where_in($this->other_key(), $ids)->delete();
113: }
114:
115: /**
116: * Sync the joining table with the array of given IDs.
117: *
118: * @param array $ids
119: * @return bool
120: */
121: public function sync($ids)
122: {
123: $current = $this->pivot()->lists($this->other_key());
124: $ids = (array) $ids;
125:
126: // First we need to attach any of the associated models that are not currently
127: // in the joining table. We'll spin through the given IDs, checking to see
128: // if they exist in the array of current ones, and if not we insert.
129: foreach ($ids as $id)
130: {
131: if ( ! in_array($id, $current))
132: {
133: $this->attach($id);
134: }
135: }
136:
137: // Next we will take the difference of the current and given IDs and detach
138: // all of the entities that exists in the current array but are not in
139: // the array of IDs given to the method, finishing the sync.
140: $detach = array_diff($current, $ids);
141:
142: if (count($detach) > 0)
143: {
144: $this->detach($detach);
145: }
146: }
147:
148: /**
149: * Insert a new record for the association.
150: *
151: * @param Model|array $attributes
152: * @param array $joining
153: * @return bool
154: */
155: public function insert($attributes, $joining = array())
156: {
157: // If the attributes are actually an instance of a model, we'll just grab the
158: // array of attributes off of the model for saving, allowing the developer
159: // to easily validate the joining models before inserting them.
160: if ($attributes instanceof Model)
161: {
162: $attributes = $attributes->attributes;
163: }
164:
165: $model = $this->model->create($attributes);
166:
167: // If the insert was successful, we'll insert a record into the joining table
168: // using the new ID that was just inserted into the related table, allowing
169: // the developer to not worry about maintaining the join table.
170: if ($model instanceof Model)
171: {
172: $joining = array_merge($this->join_record($model->get_key()), $joining);
173:
174: $result = $this->insert_joining($joining);
175: }
176:
177: return $model instanceof Model and $result;
178: }
179:
180: /**
181: * Delete all of the records from the joining table for the model.
182: *
183: * @return int
184: */
185: public function delete()
186: {
187: return $this->pivot()->delete();
188: }
189:
190: /**
191: * Create an array representing a new joining record for the association.
192: *
193: * @param int $id
194: * @return array
195: */
196: protected function join_record($id)
197: {
198: return array($this->foreign_key() => $this->base->get_key(), $this->other_key() => $id);
199: }
200:
201: /**
202: * Insert a new record into the joining table of the association.
203: *
204: * @param array $attributes
205: * @return void
206: */
207: protected function insert_joining($attributes)
208: {
209: if (Pivot::$timestamps)
210: {
211: $attributes['created_at'] = new \DateTime;
212:
213: $attributes['updated_at'] = $attributes['created_at'];
214: }
215:
216: return $this->joining_table()->insert($attributes);
217: }
218:
219: /**
220: * Get a fluent query for the joining table of the relationship.
221: *
222: * @return Query
223: */
224: protected function joining_table()
225: {
226: return $this->connection()->table($this->joining);
227: }
228:
229: /**
230: * Set the proper constraints on the relationship table.
231: *
232: * @return void
233: */
234: protected function constrain()
235: {
236: $other = $this->other_key();
237:
238: $foreign = $this->foreign_key();
239:
240: $this->set_select($foreign, $other)->set_join($other)->set_where($foreign);
241: }
242:
243: /**
244: * Set the SELECT clause on the query builder for the relationship.
245: *
246: * @param string $foreign
247: * @param string $other
248: * @return void
249: */
250: protected function set_select($foreign, $other)
251: {
252: $columns = array($this->model->table().'.*');
253:
254: $this->with = array_merge($this->with, array($foreign, $other));
255:
256: // Since pivot tables may have extra information on them that the developer
257: // needs we allow an extra array of columns to be specified that will be
258: // fetched from the pivot table and hydrate into the pivot model.
259: foreach ($this->with as $column)
260: {
261: $columns[] = $this->joining.'.'.$column.' as pivot_'.$column;
262: }
263:
264: $this->table->select($columns);
265:
266: return $this;
267: }
268:
269: /**
270: * Set the JOIN clause on the query builder for the relationship.
271: *
272: * @param string $other
273: * @return void
274: */
275: protected function set_join($other)
276: {
277: $this->table->join($this->joining, $this->associated_key(), '=', $this->joining.'.'.$other);
278:
279: return $this;
280: }
281:
282: /**
283: * Set the WHERE clause on the query builder for the relationship.
284: *
285: * @param string $foreign
286: * @return void
287: */
288: protected function set_where($foreign)
289: {
290: $this->table->where($this->joining.'.'.$foreign, '=', $this->base->get_key());
291:
292: return $this;
293: }
294:
295: /**
296: * Initialize a relationship on an array of parent models.
297: *
298: * @param array $parents
299: * @param string $relationship
300: * @return void
301: */
302: public function initialize(&$parents, $relationship)
303: {
304: foreach ($parents as &$parent)
305: {
306: $parent->relationships[$relationship] = array();
307: }
308: }
309:
310: /**
311: * Set the proper constraints on the relationship table for an eager load.
312: *
313: * @param array $results
314: * @return void
315: */
316: public function eagerly_constrain($results)
317: {
318: $this->table->where_in($this->joining.'.'.$this->foreign_key(), $this->keys($results));
319: }
320:
321: /**
322: * Match eagerly loaded child models to their parent models.
323: *
324: * @param array $parents
325: * @param array $children
326: * @return void
327: */
328: public function match($relationship, &$parents, $children)
329: {
330: $foreign = $this->foreign_key();
331:
332: $dictionary = array();
333:
334: foreach ($children as $child)
335: {
336: $dictionary[$child->pivot->$foreign][] = $child;
337: }
338:
339: foreach ($parents as $parent)
340: {
341: if (array_key_exists($key = $parent->get_key(), $dictionary))
342: {
343: $parent->relationships[$relationship] = $dictionary[$key];
344: }
345: }
346: }
347:
348: /**
349: * Hydrate the Pivot model on an array of results.
350: *
351: * @param array $results
352: * @return void
353: */
354: protected function hydrate_pivot(&$results)
355: {
356: foreach ($results as &$result)
357: {
358: // Every model result for a many-to-many relationship needs a Pivot instance
359: // to represent the pivot table's columns. Sometimes extra columns are on
360: // the pivot table that may need to be accessed by the developer.
361: $pivot = new Pivot($this->joining, $this->model->connection());
362:
363: // If the attribute key starts with "pivot_", we know this is a column on
364: // the pivot table, so we will move it to the Pivot model and purge it
365: // from the model since it actually belongs to the pivot model.
366: foreach ($result->attributes as $key => $value)
367: {
368: if (starts_with($key, 'pivot_'))
369: {
370: $pivot->{substr($key, 6)} = $value;
371:
372: $result->purge($key);
373: }
374: }
375:
376: // Once we have completed hydrating the pivot model instance, we'll set
377: // it on the result model's relationships array so the developer can
378: // quickly and easily access any pivot table information.
379: $result->relationships['pivot'] = $pivot;
380:
381: $pivot->sync() and $result->sync();
382: }
383: }
384:
385: /**
386: * Set the columns on the joining table that should be fetched.
387: *
388: * @param array $column
389: * @return Relationship
390: */
391: public function with($columns)
392: {
393: $columns = (is_array($columns)) ? $columns : func_get_args();
394:
395: // The "with" array contains a couple of columns by default, so we will just
396: // merge in the developer specified columns here, and we will make sure
397: // the values of the array are unique to avoid duplicates.
398: $this->with = array_unique(array_merge($this->with, $columns));
399:
400: $this->set_select($this->foreign_key(), $this->other_key());
401:
402: return $this;
403: }
404:
405: /**
406: * Get a relationship instance of the pivot table.
407: *
408: * @return Has_Many
409: */
410: public function pivot()
411: {
412: $pivot = new Pivot($this->joining, $this->model->connection());
413:
414: return new Has_Many($this->base, $pivot, $this->foreign_key());
415: }
416:
417: /**
418: * Get the other or associated key for the relationship.
419: *
420: * @return string
421: */
422: protected function other_key()
423: {
424: return Relationship::foreign($this->model, $this->other);
425: }
426:
427: /**
428: * Get the fully qualified associated table's primary key.
429: *
430: * @return string
431: */
432: protected function associated_key()
433: {
434: return $this->model->table().'.'.$this->model->key();
435: }
436:
437: }