has_many_and_belongs_to.php 10 KB

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