has_many_and_belongs_to.php 10.2 KB
Newer Older
Taylor Otwell committed
1 2
<?php namespace Laravel\Database\Eloquent\Relationships;

3
use Laravel\Str;
4
use Laravel\Database\Eloquent\Model;
5 6
use Laravel\Database\Eloquent\Pivot;

Taylor Otwell committed
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
class Has_Many_And_Belongs_To extends Relationship {

	/**
	 * The name of the intermediate, joining table.
	 *
	 * @var string
	 */
	protected $joining;

	/**
	 * The other or "associated" key. This is the foreign key of the related model.
	 *
	 * @var string
	 */
	protected $other;

	/**
24
	 * The columns on the joining table that should be fetched.
25 26 27
	 *
	 * @var array
	 */
28
	protected $with = array('id');
29 30

	/**
Taylor Otwell committed
31 32 33 34 35 36 37 38 39 40 41 42 43
	 * Create a new many to many relationship instance.
	 *
	 * @param  Model   $model
	 * @param  string  $associated
	 * @param  string  $table
	 * @param  string  $foreign
	 * @param  string  $other
	 * @return void
	 */
	public function __construct($model, $associated, $table, $foreign, $other)
	{
		$this->other = $other;

44
		$this->joining = $table ?: $this->joining($model, $associated);
Taylor Otwell committed
45

46 47 48
		// If the Pivot table is timestamped, we'll set the timestamp columns to be
		// fetched when the pivot table models are fetched by the developer else
		// the ID will be the only "extra" column fetched in by default.
49 50
		if (Pivot::$timestamps)
		{
51 52 53
			$this->with[] = 'created_at';

			$this->with[] = 'updated_at';
54 55
		}

Taylor Otwell committed
56 57 58 59
		parent::__construct($model, $associated, $foreign);
	}

	/**
60 61
	 * Determine the joining table name for the relationship.
	 *
62
	 * By default, the name is the models sorted and joined with underscores.
63 64 65 66 67 68 69 70 71 72 73 74 75
	 *
	 * @return string
	 */
	protected function joining($model, $associated)
	{
		$models = array(class_basename($model), class_basename($associated));

		sort($models);

		return strtolower($models[0].'_'.$models[1]);
	}

	/**
Taylor Otwell committed
76 77 78 79 80 81 82 83 84 85 86 87
	 * Get the properly hydrated results for the relationship.
	 *
	 * @return array
	 */
	public function results()
	{
		return parent::get();
	}

	/**
	 * Insert a new record into the joining table of the association.
	 *
88 89
	 * @param  int    $id
	 * @param  array  $joining
Taylor Otwell committed
90 91
	 * @return bool
	 */
92
	public function attach($id, $attributes = array())
Taylor Otwell committed
93
	{
94 95 96
		$joining = array_merge($this->join_record($id), $attributes);

		return $this->insert_joining($joining);
Taylor Otwell committed
97 98 99
	}

	/**
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
	 * Detach a record from the joining table of the association.
	 *
	 * @param  int   $ids
	 * @return bool
	 */
	public function detach($ids)
	{
		if ( ! is_array($ids)) $ids = array($ids);

		return $this->pivot()->where_in($this->other_key(), $ids)->delete();
	}

	/**
	 * Sync the joining table with the array of given IDs.
	 *
	 * @param  array  $ids
	 * @return bool
	 */
	public function sync($ids)
	{
		$current = $this->pivot()->lists($this->other_key());

		// First we need to attach any of the associated models that are not currently
		// in the joining table. We'll spin through the given IDs, checking to see
		// if they exist in the array of current ones, and if not we insert.
		foreach ($ids as $id)
		{
			if ( ! in_array($id, $current))
			{
				$this->attach($id);
			}
		}

		// Next we will take the difference of the current and given IDs and detach
		// all of the entities that exists in the current array but are not in
		// the array of IDs given to the method, finishing the sync.
		$detach = array_diff($current, $ids);

		if (count($detach) > 0)
		{
			$this->detach(array_diff($current, $ids));
		}
	}

	/**
Taylor Otwell committed
145 146
	 * Insert a new record for the association.
	 *
147 148
	 * @param  Model|array  $attributes
	 * @param  array        $joining
Taylor Otwell committed
149 150
	 * @return bool
	 */
151
	public function insert($attributes, $joining = array())
Taylor Otwell committed
152
	{
153 154 155 156 157 158 159 160
		// If the attributes are actually an instance of a model, we'll just grab the
		// array of attributes off of the model for saving, allowing the developer
		// to easily validate the joining models before inserting them.
		if ($attributes instanceof Model)
		{
			$attributes = $attributes->attributes;
		}

161
		$model = $this->model->create($attributes);
Taylor Otwell committed
162

163 164 165
		// If the insert was successful, we'll insert a record into the joining table
		// using the new ID that was just inserted into the related table, allowing
		// the developer to not worry about maintaining the join table.
166
		if ($model instanceof Model)
167
		{
168
			$joining = array_merge($this->join_record($model->get_key()), $joining);
169 170

			$result = $this->insert_joining($joining);
171
		}
Taylor Otwell committed
172

173
		return $model instanceof Model and $result;
Taylor Otwell committed
174 175 176 177 178 179 180 181 182
	}

	/**
	 * Delete all of the records from the joining table for the model.
	 *
	 * @return int
	 */
	public function delete()
	{
183
		return $this->pivot()->delete();
Taylor Otwell committed
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
	}

	/**
	 * Create an array representing a new joining record for the association.
	 *
	 * @param  int    $id
	 * @return array
	 */
	protected function join_record($id)
	{
		return array($this->foreign_key() => $this->base->get_key(), $this->other_key() => $id);
	}

	/**
	 * Insert a new record into the joining table of the association.
	 *
	 * @param  array  $attributes
	 * @return void
	 */
	protected function insert_joining($attributes)
	{
205 206
		if (Pivot::$timestamps)
		{
207
			$attributes['created_at'] = new \DateTime;
208

209 210
			$attributes['updated_at'] = $attributes['created_at'];
		}
211

Taylor Otwell committed
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
		return $this->joining_table()->insert($attributes);
	}

	/**
	 * Get a fluent query for the joining table of the relationship.
	 *
	 * @return Query
	 */
	protected function joining_table()
	{
		return $this->connection()->table($this->joining);
	}

	/**
	 * Set the proper constraints on the relationship table.
	 *
	 * @return void
	 */
	protected function constrain()
	{
232 233
		$other = $this->other_key();

Taylor Otwell committed
234 235
		$foreign = $this->foreign_key();

236
		$this->set_select($foreign, $other)->set_join($other)->set_where($foreign);
Taylor Otwell committed
237 238 239 240 241 242
	}

	/**
	 * Set the SELECT clause on the query builder for the relationship.
	 *
	 * @param  string  $foreign
243
	 * @param  string  $other
Taylor Otwell committed
244 245
	 * @return void
	 */
246
	protected function set_select($foreign, $other)
Taylor Otwell committed
247
	{
248 249 250 251 252
		$columns = array($this->model->table().'.*');

		$this->with = array_merge($this->with, array($foreign, $other));

		// Since pivot tables may have extra information on them that the developer
Taylor Otwell committed
253
		// needs we allow an extra array of columns to be specified that will be
254 255 256 257 258
		// fetched from the pivot table and hydrate into the pivot model.
		foreach ($this->with as $column)
		{
			$columns[] = $this->joining.'.'.$column.' as pivot_'.$column;
		}
Taylor Otwell committed
259

260
		$this->table->select($columns);
Taylor Otwell committed
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313

		return $this;
	}

	/**
	 * Set the JOIN clause on the query builder for the relationship.
	 *
	 * @param  string  $other
	 * @return void
	 */
	protected function set_join($other)
	{
		$this->table->join($this->joining, $this->associated_key(), '=', $this->joining.'.'.$other);

		return $this;
	}

	/**
	 * Set the WHERE clause on the query builder for the relationship.
	 *
	 * @param  string  $foreign
	 * @return void
	 */
	protected function set_where($foreign)
	{
		$this->table->where($this->joining.'.'.$foreign, '=', $this->base->get_key());

		return $this;
	}

	/**
	 * Initialize a relationship on an array of parent models.
	 *
	 * @param  array   $parents
	 * @param  string  $relationship
	 * @return void
	 */
	public function initialize(&$parents, $relationship)
	{
		foreach ($parents as &$parent)
		{
			$parent->relationships[$relationship] = array();
		}
	}

	/**
	 * Set the proper constraints on the relationship table for an eager load.
	 *
	 * @param  array  $results
	 * @return void
	 */
	public function eagerly_constrain($results)
	{
314
		$this->table->where_in($this->joining.'.'.$this->foreign_key(), $this->keys($results));
Taylor Otwell committed
315 316 317 318 319 320 321 322 323 324 325
	}

	/**
	 * Match eagerly loaded child models to their parent models.
	 *
	 * @param  array  $parents
	 * @param  array  $children
	 * @return void
	 */
	public function match($relationship, &$parents, $children)
	{
326
		$foreign = $this->foreign_key();
Taylor Otwell committed
327

328 329 330 331 332 333 334
		$dictionary = array();

		foreach ($children as $child)
		{
			$dictionary[$child->pivot->$foreign][] = $child;
		}

335
		foreach ($parents as &$parent)
Taylor Otwell committed
336
		{
337
			$parent_key = $parent->get_key();
Taylor Otwell committed
338

339 340 341 342
			if (isset($dictionary[$parent_key]))
			{
				$parent->relationships[$relationship] = $dictionary[$parent_key];
			}
Taylor Otwell committed
343 344 345 346
		}
	}

	/**
347
	 * Hydrate the Pivot model on an array of results.
Taylor Otwell committed
348 349 350 351
	 *
	 * @param  array  $results
	 * @return void
	 */
352
	protected function hydrate_pivot(&$results)
Taylor Otwell committed
353 354 355
	{
		foreach ($results as &$result)
		{
356 357 358
			// Every model result for a many-to-many relationship needs a Pivot instance
			// to represent the pivot table's columns. Sometimes extra columns are on
			// the pivot table that may need to be accessed by the developer.
359
			$pivot = new Pivot($this->joining, $this->model->connection());
360 361 362

			// If the attribute key starts with "pivot_", we know this is a column on
			// the pivot table, so we will move it to the Pivot model and purge it
Taylor Otwell committed
363
			// from the model since it actually belongs to the pivot model.
364
			foreach ($result->attributes as $key => $value)
365
			{
366 367 368
				if (starts_with($key, 'pivot_'))
				{
					$pivot->{substr($key, 6)} = $value;
369

370 371
					$result->purge($key);
				}
372 373 374 375 376 377 378 379
			}

			// Once we have completed hydrating the pivot model instance, we'll set
			// it on the result model's relationships array so the developer can
			// quickly and easily access any pivot table information.
			$result->relationships['pivot'] = $pivot;

			$pivot->sync() and $result->sync();
Taylor Otwell committed
380 381 382 383
		}
	}

	/**
384 385 386 387 388 389 390
	 * Set the columns on the joining table that should be fetched.
	 *
	 * @param  array         $column
	 * @return Relationship
	 */
	public function with($columns)
	{
391 392
		$columns = (is_array($columns)) ? $columns : func_get_args();

Taylor Otwell committed
393 394 395
		// The "with" array contains a couple of columns by default, so we will just
		// merge in the developer specified columns here, and we will make sure
		// the values of the array are unique to avoid duplicates.
396
		$this->with = array_unique(array_merge($this->with, $columns));
397 398 399 400 401 402 403

		$this->set_select($this->foreign_key(), $this->other_key());

		return $this;
	}

	/**
404
	 * Get a relationship instance of the pivot table.
405
	 *
406
	 * @return Has_Many
407 408 409
	 */
	public function pivot()
	{
410 411 412
		$pivot = new Pivot($this->joining, $this->model->connection());

		return new Has_Many($this->base, $pivot, $this->foreign_key());
413 414 415
	}

	/**
Taylor Otwell committed
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435
	 * Get the other or associated key for the relationship.
	 *
	 * @return string
	 */
	protected function other_key()
	{
		return Relationship::foreign($this->model, $this->other);
	}

	/**
	 * Get the fully qualified associated table's primary key.
	 *
	 * @return string
	 */
	protected function associated_key()
	{
		return $this->model->table().'.'.$this->model->key();
	}

}