has_many_and_belongs_to.php 9.8 KB

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