PluginProbe ʕ •ᴥ•ʔ
Kirki – Freeform Page Builder, Website Builder & Customizer / 6.1.0
Kirki – Freeform Page Builder, Website Builder & Customizer v6.1.0
6.1.1 6.1.0 6.0.14 6.0.13 6.0.12 6.0.11 6.0.10 6.0.9 6.0.8 6.0.7 6.0.6 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 4.0.19 4.0.20 4.0.21 4.0.22 4.0.23 4.0.24 4.1 4.2.0 5.0.0 5.1.0 5.1.1 5.2.0 5.2.1 5.2.2 5.2.3 6.0.0 trunk 3.0.40 3.0.41 3.0.42 3.0.43 3.0.44 3.0.45 3.1.0 3.1.1 3.1.2
kirki / libraries / framework / Database / Query / Relations / BelongsToMany.php
kirki / libraries / framework / Database / Query / Relations Last commit date
BelongsTo.php 3 weeks ago BelongsToMany.php 3 weeks ago HasMany.php 3 weeks ago HasOne.php 3 weeks ago Relation.php 3 weeks ago
BelongsToMany.php
890 lines
1 <?php
2
3 /**
4 * Define a many-to-many relation using a pivot table.
5 * Joins the related table through a pivot, adding constraints and selections for pivot keys and optional pivot columns.
6 * Supports eager loading, matching results, and managing pivot records via attach, detach, sync, and toggle.
7 *
8 * @package Framework
9 * @subpackage Database\Query\Relations
10 * @since 1.0.0
11 */
12 namespace Kirki\Framework\Database\Query\Relations;
13
14 \defined('ABSPATH') || exit;
15 use Kirki\Framework\Database\Query\Model;
16 use Kirki\Framework\Database\Query\Collection;
17 use Kirki\Framework\Database\Concerns\HasDictionary;
18 use Kirki\Framework\Database\Query\QueryBuilder;
19 use function Kirki\Framework\collection;
20 use function Kirki\Framework\Polyfill\str_contains;
21 use function Kirki\Framework\Polyfill\str_starts_with;
22 class BelongsToMany extends Relation
23 {
24 use HasDictionary;
25 /**
26 * The name of the pivot table used for the many-to-many relationship.
27 *
28 * @var string
29 *
30 * @since 1.0.0
31 */
32 protected $pivot_table;
33 /**
34 * The foreign key column name on the pivot table referencing the parent model.
35 *
36 * @var string
37 *
38 * @since 1.0.0
39 */
40 protected $foreign_pivot_key;
41 /**
42 * The foreign key column name on the pivot table referencing the related model.
43 *
44 * @var string
45 *
46 * @since 1.0.0
47 */
48 protected $related_pivot_key;
49 /**
50 * The primary key name on the parent model.
51 *
52 * @var string
53 *
54 * @since 1.0.0
55 */
56 protected $parent_key;
57 /**
58 * The primary key name on the related model.
59 *
60 * @var string
61 *
62 * @since 1.0.0
63 */
64 protected $related_key;
65 /**
66 * The additional columns to be selected from the pivot table.
67 *
68 * @var array
69 *
70 * @since 1.0.0
71 */
72 protected $pivot_columns = [];
73 /**
74 * The additional where conditions to be applied on the pivot table.
75 *
76 * @var array
77 *
78 * @since 1.0.0
79 */
80 protected $pivot_wheres = [];
81 /**
82 * The additional where in conditions to be applied on the pivot table.
83 *
84 * @var array
85 *
86 * @since 1.0.0
87 */
88 protected $pivot_where_ins = [];
89 /**
90 * The additional where null conditions to be applied on the pivot table.
91 *
92 * @var array
93 *
94 * @since 1.0.0
95 */
96 protected $pivot_where_nulls = [];
97 /**
98 * The name of the relation.
99 *
100 * @var string|null
101 *
102 * @since 1.0.0
103 */
104 protected $relation_name;
105 /**
106 * Create a new belongs-to-many relation instance.
107 *
108 * Stores pivot and key names, applies default join and base constraints so
109 * that the relation query returns related models with pivot context.
110 *
111 * @param Model $related The related model instance
112 * @param Model $parent The parent model instance
113 * @param mixed $pivot_table The pivot table name
114 * @param mixed $foreign_pivot_key The pivot column for the parent key
115 * @param mixed $related_pivot_key The pivot column for the related key
116 * @param mixed $parent_key The local key name on the parent model
117 * @param mixed $related_key The key name on the related model
118 *
119 * @return void No return value
120 *
121 * @since 1.0.0
122 */
123 public function __construct(Model $related, Model $parent, $pivot_table, $foreign_pivot_key, $related_pivot_key, $parent_key, $related_key, $relation_name = null)
124 {
125 $this->pivot_table = $pivot_table;
126 $this->foreign_pivot_key = $foreign_pivot_key;
127 $this->related_pivot_key = $related_pivot_key;
128 $this->parent_key = $parent_key;
129 $this->related_key = $related_key;
130 $this->relation_name = $relation_name;
131 parent::__construct($related, $parent);
132 }
133 /**
134 * Apply base constraints and join through the pivot table.
135 *
136 * Ensures the relation query joins the pivot and filters using the
137 * parent's key when available so only related records are returned.
138 *
139 * @return void
140 *
141 * @since 1.0.0
142 */
143 public function add_constraints()
144 {
145 if (static::$constraints) {
146 $this->perform_join();
147 $this->add_where_constraints();
148 }
149 }
150 /**
151 * Add the where constraints to the query.
152 *
153 * @return void
154 *
155 * @since 1.0.0
156 */
157 protected function add_where_constraints()
158 {
159 $this->query->where($this->get_qualified_foreign_pivot_key_name(), '=', $this->parent->{$this->parent_key});
160 }
161 /**
162 * Get the qualified foreign pivot key name.
163 *
164 * @return string The qualified foreign pivot key name
165 *
166 * @since 1.0.0
167 */
168 public function get_qualified_foreign_pivot_key_name()
169 {
170 return $this->qualify_pivot_column($this->foreign_pivot_key);
171 }
172 /**
173 * Get the aggregate query for the relation.
174 *
175 * @param QueryBuilder $query The query builder instance
176 * @param QueryBuilder $parent The parent query builder instance
177 * @param mixed $columns The columns to select
178 *
179 * @return QueryBuilder The aggregate query
180 *
181 * @since 1.0.0
182 */
183 public function get_relation_existence_query(QueryBuilder $query, QueryBuilder $parent, $columns = ['*'])
184 {
185 if ($parent->from === $query->from) {
186 return $this->get_relation_existence_query_for_self_join($query, $parent, $columns);
187 }
188 $this->perform_join($query);
189 return parent::get_relation_existence_query($query, $parent, $columns);
190 }
191 /**
192 * Get the aggregate query for the relation for self join.
193 *
194 * @param QueryBuilder $query The query builder instance
195 * @param QueryBuilder $parent The parent query builder instance
196 * @param mixed $columns The columns to select
197 *
198 * @return QueryBuilder The aggregate query
199 *
200 * @since 1.0.0
201 */
202 public function get_relation_existence_query_for_self_join(QueryBuilder $query, QueryBuilder $parent, $columns = ['*'])
203 {
204 $query->select($columns);
205 $query->from($this->related->get_table() . ' as ' . ($hash = $this->get_relation_count_hash()));
206 $this->related->set_table($hash);
207 $this->perform_join($query);
208 return parent::get_relation_existence_query($query, $parent, $columns);
209 }
210 /**
211 * Get the existence compare key for the relation.
212 *
213 * @return string The existence compare key
214 *
215 * @since 1.0.0
216 */
217 public function get_existence_compare_key()
218 {
219 return $this->qualify_pivot_column($this->foreign_pivot_key);
220 }
221 /**
222 * Configure the join and default select columns for the relation.
223 *
224 * Selects all related columns along with the foreign pivot key aliased for
225 * matching. Establishes the join between related and pivot tables.
226 *
227 * @param mixed $query The query builder instance.
228 *
229 * @return void
230 *
231 * @since 1.0.0
232 */
233 protected function perform_join($query = null)
234 {
235 $query = $query ?: $this->query;
236 $related_columns = $this->related_columns();
237 $query->select([...$related_columns, ...$this->qualify_pivot_columns()]);
238 $query->join($this->pivot_table, $this->get_qualified_related_key_name(), '=', $this->get_qualified_related_pivot_key_name());
239 }
240 /**
241 * Get the qualified related key name.
242 *
243 * @return string The qualified related key name
244 *
245 * @since 1.0.0
246 */
247 protected function get_qualified_related_key_name()
248 {
249 return $this->related->prepare_column($this->related_key);
250 }
251 /**
252 * Get the qualified related pivot key name.
253 *
254 * @return string The qualified related pivot key name
255 *
256 * @since 1.0.0
257 */
258 protected function get_qualified_related_pivot_key_name()
259 {
260 return $this->qualify_pivot_column($this->related_pivot_key);
261 }
262 /**
263 * Get the related columns for the query.
264 *
265 * @return array The related columns
266 *
267 * @since 1.0.0
268 */
269 protected function related_columns()
270 {
271 return collection($this->query->columns ? $this->query->columns : ['*'])->map(function ($column) {
272 return $this->prepare_related_column($column);
273 })->all();
274 }
275 /**
276 * Prepare the related column for the query.
277 *
278 * @param string $column The column to prepare
279 *
280 * @return string The prepared column
281 *
282 * @since 1.0.0
283 */
284 protected function prepare_related_column($column)
285 {
286 if ($this->query->get_compiler()->is_expression($column)) {
287 return $column;
288 }
289 $related_table = $this->related->get_table();
290 return str_contains($column, '.') ? $column : $related_table . '.' . $column;
291 }
292 /**
293 * Prepare pivot columns for the query.
294 *
295 * @return array The prepared pivot columns
296 *
297 * @since 1.0.0
298 */
299 protected function qualify_pivot_columns()
300 {
301 return collection(\array_filter([$this->foreign_pivot_key, $this->related_pivot_key, ...$this->pivot_columns], function ($column) {
302 return $column !== null;
303 }))->map(function ($column) {
304 return $this->qualify_pivot_column($column) . ' as pivot_' . $column;
305 })->all();
306 }
307 /**
308 * Get the related results for lazy loading.
309 *
310 * Returns a collection of related models, including any selected pivot
311 * columns as aliases prefixed with pivot_.
312 *
313 * @return Collection The collection of related models
314 *
315 * @since 1.0.0
316 */
317 public function get_results()
318 {
319 return !\is_null($this->parent->{$this->parent_key}) ? $this->get() : $this->related->new_collection();
320 }
321 /**
322 * Add eager loading constraints across the given parents.
323 *
324 * Scopes the query by collecting parent keys and applying a where_in on the
325 * foreign pivot key so all related models can be fetched at once.
326 *
327 * @param Collection $models The parent models to constrain by
328 *
329 * @return void
330 *
331 * @since 1.0.0
332 */
333 public function add_eager_constraints(Collection $models)
334 {
335 $this->perform_join();
336 $this->where_in_eager($this->get_qualified_foreign_pivot_key_name(), $this->get_keys($models, $this->parent_key));
337 }
338 /**
339 * @inheritdoc
340 */
341 public function init_relation(Collection $models, $relation)
342 {
343 foreach ($models as $model) {
344 $model->set_relation($relation, $this->related->new_collection());
345 }
346 return $models;
347 }
348 /**
349 * Match eager loaded related models back to their parents.
350 *
351 * Builds a dictionary keyed by the foreign pivot key and assigns a
352 * collection to each parent under the relation name, defaulting to an
353 * empty collection when none exist.
354 *
355 * @param Collection $models The parent models receiving results
356 * @param Collection $results The results.
357 * @param string $relation The relation name on the parent
358 *
359 * @return Collection The parent models with relations populated
360 *
361 * @since 1.0.0
362 */
363 public function match(Collection $models, Collection $results, $relation)
364 {
365 $dictionary = $this->build_dictionary($results);
366 foreach ($models as $model) {
367 $attribute = $this->get_dictionary_key($model->get_attribute($this->parent_key));
368 if ($attribute !== null && isset($dictionary[$attribute])) {
369 $model->set_relation($relation, $this->related->new_collection($dictionary[$attribute]));
370 }
371 }
372 return $models;
373 }
374 /**
375 * Build the dictionary for the relation from the results.
376 *
377 * @param Collection $results The results.
378 *
379 * @return array The dictionary
380 *
381 * @since 1.0.0
382 */
383 protected function build_dictionary(Collection $results)
384 {
385 $dictionary = [];
386 foreach ($results as $result) {
387 $pivot_key_name = "pivot_{$this->foreign_pivot_key}";
388 $attribute = $this->get_dictionary_key($result->get_attribute($pivot_key_name));
389 if ($attribute !== null) {
390 $dictionary[$attribute] ??= [];
391 $dictionary[$attribute][] = $this->migrate_pivot_attributes($result);
392 }
393 }
394 return $dictionary;
395 }
396 /**
397 * Migrate pivot attributes to the model.
398 *
399 * @param Model $model The model to migrate the pivot attributes to
400 *
401 * @return Model The model with the migrated pivot attributes
402 *
403 * @since 1.0.0
404 */
405 protected function migrate_pivot_attributes(Model $model) : Model
406 {
407 $values = [];
408 foreach ($model->get_attributes() as $key => $value) {
409 if (str_starts_with($key, 'pivot_')) {
410 $values[\substr($key, 6)] = $value;
411 unset($model->{$key});
412 }
413 }
414 $model->set_attribute('pivot', $values);
415 return $model;
416 }
417 /**
418 * Include additional pivot columns in the select.
419 *
420 * Accepts a single column, array, or variadic list and adds aliased
421 * selections for each as pivot_<name> to the relation query.
422 *
423 * @param mixed $columns The pivot column name(s) to include
424 *
425 * @return $this The relation instance for method chaining
426 *
427 * @since 1.0.0
428 */
429 public function with_pivot($columns)
430 {
431 $this->pivot_columns = \array_merge($this->pivot_columns, \is_array($columns) ? $columns : \func_get_args());
432 return $this;
433 }
434 /**
435 * Add a where clause scoped to the pivot table.
436 *
437 * Prefixes the provided column with the pivot table and forwards the
438 * condition to the underlying query builder for filtering.
439 *
440 * @param string $column The pivot column to filter
441 * @param mixed $operator The comparison operator or value when two args
442 * @param mixed $value The value to compare against when operator provided
443 * @param mixed $boolean The boolean.
444 *
445 * @return $this The relation instance for method chaining
446 *
447 * @since 1.0.0
448 */
449 public function where_pivot($column, $operator = null, $value = null, $boolean = 'and')
450 {
451 $this->pivot_wheres[] = \func_get_args();
452 $this->where($this->qualify_pivot_column($column), $operator, $value, $boolean);
453 return $this;
454 }
455 /**
456 * Add a "where" clause on the pivot table with "or" boolean.
457 *
458 * @param mixed $column The column.
459 * @param mixed $operator The operator.
460 * @param mixed $value The value.
461 *
462 * @return $this
463 *
464 * @since 1.0.0
465 */
466 public function or_where_pivot($column, $operator = null, $value = null)
467 {
468 return $this->where_pivot($column, $operator, $value, 'or');
469 }
470 /**
471 * Add a "where between" clause on the pivot table.
472 *
473 * @param mixed $column The column.
474 * @param array $values The values.
475 * @param mixed $boolean The boolean.
476 * @param mixed $not The not.
477 *
478 * @return $this
479 *
480 * @since 1.0.0
481 */
482 public function where_pivot_between($column, array $values, $boolean = 'and', $not = \false)
483 {
484 return $this->where_between($this->qualify_pivot_column($column), $values, $boolean, $not);
485 }
486 /**
487 * Add an "or where between" clause on the pivot table.
488 *
489 * @param mixed $column The column.
490 * @param array $values The values.
491 *
492 * @return $this
493 *
494 * @since 1.0.0
495 */
496 public function or_where_pivot_between($column, array $values)
497 {
498 return $this->where_pivot_between($column, $values, 'or');
499 }
500 /**
501 * Add a "where not between" clause on the pivot table.
502 *
503 * @param mixed $column The column.
504 * @param array $values The values.
505 * @param mixed $boolean The boolean.
506 *
507 * @return $this
508 *
509 * @since 1.0.0
510 */
511 public function where_pivot_not_between($column, array $values, $boolean = 'and')
512 {
513 return $this->where_pivot_between($column, $values, $boolean, \true);
514 }
515 /**
516 * Add an "or where not between" clause on the pivot table.
517 *
518 * @param mixed $column The column.
519 * @param array $values The values.
520 *
521 * @return $this
522 *
523 * @since 1.0.0
524 */
525 public function or_where_pivot_not_between($column, array $values)
526 {
527 return $this->where_pivot_not_between($column, $values, 'or');
528 }
529 /**
530 * Add a "where in" clause on the pivot table.
531 *
532 * @param mixed $column The column.
533 * @param mixed $values The values.
534 * @param mixed $boolean The boolean.
535 * @param mixed $not The not.
536 *
537 * @return $this
538 *
539 * @since 1.0.0
540 */
541 public function where_pivot_in($column, $values, $boolean = 'and', $not = \false)
542 {
543 $this->pivot_where_ins[] = \func_get_args();
544 return $this->where_in($this->qualify_pivot_column($column), $values, $boolean, $not);
545 }
546 /**
547 * Add an "or where in" clause on the pivot table.
548 *
549 * @param mixed $column The column.
550 * @param mixed $values The values.
551 *
552 * @return $this
553 *
554 * @since 1.0.0
555 */
556 public function or_where_pivot_in($column, $values)
557 {
558 return $this->where_pivot_in($column, $values, 'or');
559 }
560 /**
561 * Add a "where not in" clause on the pivot table.
562 *
563 * @param mixed $column The column.
564 * @param mixed $values The values.
565 * @param mixed $boolean The boolean.
566 *
567 * @return $this
568 *
569 * @since 1.0.0
570 */
571 public function where_pivot_not_in($column, $values, $boolean = 'and')
572 {
573 return $this->where_pivot_in($column, $values, $boolean, \true);
574 }
575 /**
576 * Add an "or where not in" clause on the pivot table.
577 *
578 * @param mixed $column The column.
579 * @param mixed $values The values.
580 *
581 * @return $this
582 *
583 * @since 1.0.0
584 */
585 public function or_where_pivot_not_in($column, $values)
586 {
587 return $this->where_pivot_not_in($column, $values, 'or');
588 }
589 /**
590 * Add a "where null" clause on the pivot table.
591 *
592 * @param mixed $column The column.
593 * @param mixed $boolean The boolean.
594 * @param mixed $not The not.
595 *
596 * @return $this
597 *
598 * @since 1.0.0
599 */
600 public function where_pivot_null($column, $boolean = 'and', $not = \false)
601 {
602 $this->pivot_where_nulls[] = \func_get_args();
603 return $this->where_null($this->qualify_pivot_column($column), $boolean, $not);
604 }
605 /**
606 * Add an "or where null" clause on the pivot table.
607 *
608 * @param mixed $column The column.
609 *
610 * @return $this
611 *
612 * @since 1.0.0
613 */
614 public function or_where_pivot_null($column)
615 {
616 return $this->where_pivot_null($column, 'or');
617 }
618 /**
619 * Add a "where not null" clause on the pivot table.
620 *
621 * @param mixed $column The column.
622 * @param mixed $boolean The boolean.
623 *
624 * @return $this
625 *
626 * @since 1.0.0
627 */
628 public function where_pivot_not_null($column, $boolean = 'and')
629 {
630 return $this->where_pivot_null($column, $boolean, \true);
631 }
632 /**
633 * Add an "or where not null" clause on the pivot table.
634 *
635 * @param mixed $column The column.
636 *
637 * @return $this
638 *
639 * @since 1.0.0
640 */
641 public function or_where_pivot_not_null($column)
642 {
643 return $this->where_pivot_not_null($column, 'or');
644 }
645 /**
646 * Add an "order by" clause on the pivot table.
647 *
648 * @param mixed $column The column.
649 * @param mixed $direction The direction.
650 *
651 * @return $this
652 *
653 * @since 1.0.0
654 */
655 public function order_by_pivot($column, $direction = 'asc')
656 {
657 return $this->order_by($this->qualify_pivot_column($column), $direction);
658 }
659 /**
660 * Prepare the pivot column for the query.
661 *
662 * @param mixed $column The pivot column to prepare
663 *
664 * @return string The prepared pivot column
665 *
666 * @since 1.0.0
667 */
668 protected function qualify_pivot_column($column)
669 {
670 if ($this->query->get_compiler()->is_expression($column)) {
671 return $column;
672 }
673 if ($column === null) {
674 return null;
675 }
676 return str_contains($column, '.') ? $column : $this->pivot_table . '.' . $column;
677 }
678 /**
679 * Attach related records to the parent via the pivot table.
680 *
681 * Accepts a single id or an array/map of ids to attributes and inserts the
682 * appropriate pivot records, automatically adding timestamps when enabled.
683 *
684 * @param mixed $id The related model id or array map of ids to attributes
685 * @param array $attributes Additional pivot attributes to store
686 *
687 * @return void
688 *
689 * @since 1.0.0
690 */
691 public function attach($id, array $attributes = [])
692 {
693 if (\is_array($id)) {
694 foreach ($id as $single_id => $attrs) {
695 if (\is_numeric($single_id)) {
696 $this->attach($attrs);
697 } else {
698 $this->attach($single_id, $attrs);
699 }
700 }
701 return;
702 }
703 $record = \array_merge([$this->foreign_pivot_key => $this->parent->get_attribute($this->parent_key), $this->related_pivot_key => $id], $attributes);
704 $this->add_timestamps_to_pivot($record);
705 $query = $this->parent::query();
706 $query->from($this->pivot_table)->insert($record);
707 }
708 /**
709 * Detach related records from the parent via the pivot table.
710 *
711 * Deletes pivot rows matching the parent's key and optionally limited to
712 * the given related ids when provided.
713 *
714 * @param mixed $ids Optional id or array of ids to detach
715 *
716 * @return int The number of pivot rows deleted
717 *
718 * @since 1.0.0
719 */
720 public function detach($ids = null)
721 {
722 $query = $this->parent::query()->from($this->pivot_table);
723 $query->where($this->foreign_pivot_key, '=', $this->parent->get_attribute($this->parent_key));
724 if ($ids !== null) {
725 $ids = \is_array($ids) ? $ids : [$ids];
726 $query->where_in($this->related_pivot_key, $ids);
727 }
728 return $query->delete();
729 }
730 /**
731 * Synchronize the pivot table with the given set of ids.
732 *
733 * Attaches missing, updates existing with provided attributes, and
734 * optionally detaches records not present when detaching is enabled.
735 * Returns a change summary keyed by action.
736 *
737 * @param mixed $ids Array or map of ids to attributes, or single id
738 * @param bool $detaching Whether to remove records not in the provided set
739 *
740 * @return array Summary of changes: attached, detached, updated
741 *
742 * @since 1.0.0
743 */
744 public function sync($ids, $detaching = \true)
745 {
746 $changes = ['attached' => [], 'detached' => [], 'updated' => []];
747 $current = $this->get_current_pivot_ids();
748 if (\is_array($ids)) {
749 $records = [];
750 foreach ($ids as $id => $attributes) {
751 if (!\is_array($attributes)) {
752 [$id, $attributes] = [$attributes, []];
753 }
754 $records[$id] = $attributes;
755 }
756 } else {
757 $records = \array_fill_keys((array) $ids, []);
758 }
759 $detach = \array_diff($current, \array_keys($records));
760 if ($detaching && \count($detach) > 0) {
761 $this->detach($detach);
762 $changes['detached'] = $detach;
763 }
764 foreach ($records as $id => $attributes) {
765 if (!\in_array($id, $current)) {
766 $this->attach($id, $attributes);
767 $changes['attached'][] = $id;
768 } elseif (\count($attributes) > 0) {
769 $this->update_existing_pivot($id, $attributes);
770 $changes['updated'][] = $id;
771 }
772 }
773 return $changes;
774 }
775 /**
776 * Sync the pivot records without detaching missing ones.
777 *
778 * A convenience wrapper around sync that preserves existing attachments
779 * while adding and updating as needed.
780 *
781 * @param mixed $ids Array or map of ids to attributes, or single id
782 *
783 * @return array Summary of changes from the sync operation
784 *
785 * @since 1.0.0
786 */
787 public function sync_without_detaching($ids)
788 {
789 return $this->sync($ids, \false);
790 }
791 /**
792 * Toggle the attachment state for the given ids.
793 *
794 * Attaches ids that are not currently related and detaches those that are.
795 * Returns a summary of attachments and detachments performed.
796 *
797 * @param mixed $ids One id, array of ids, or variadic list
798 *
799 * @return array Summary of attachments and detachments
800 *
801 * @since 1.0.0
802 */
803 public function toggle($ids)
804 {
805 $changes = ['attached' => [], 'detached' => []];
806 $ids = \is_array($ids) ? $ids : \func_get_args();
807 $current = $this->get_current_pivot_ids();
808 foreach ($ids as $id) {
809 if (\in_array($id, $current)) {
810 $this->detach($id);
811 $changes['detached'][] = $id;
812 } else {
813 $this->attach($id);
814 $changes['attached'][] = $id;
815 }
816 }
817 return $changes;
818 }
819 /**
820 * Retrieve the currently attached related ids from the pivot table.
821 *
822 * Queries the pivot for the related pivot key where the foreign pivot key
823 * matches the parent, and returns a plain array of ids.
824 *
825 * @return array The list of currently attached related ids
826 *
827 * @since 1.0.0
828 */
829 protected function get_current_pivot_ids()
830 {
831 $query = $this->parent::query()->from($this->pivot_table)->where($this->foreign_pivot_key, '=', $this->parent->get_attribute($this->parent_key))->select([$this->related_pivot_key]);
832 return $query->get()->pluck($this->related_pivot_key)->all();
833 }
834 /**
835 * Update attributes for an existing pivot record.
836 *
837 * Applies timestamp updates when needed and issues an update against the
838 * pivot table constrained by the parent and related keys.
839 *
840 * @param mixed $id The related model id whose pivot row to update
841 * @param array $attributes The attributes to update on the pivot row
842 *
843 * @return void
844 *
845 * @since 1.0.0
846 */
847 protected function update_existing_pivot($id, array $attributes)
848 {
849 $this->add_timestamps_to_pivot($attributes, \true);
850 $query = $this->parent::query()->from($this->pivot_table);
851 $query->where($this->foreign_pivot_key, '=', $this->parent->get_attribute($this->parent_key))->where($this->related_pivot_key, '=', $id)->update($attributes);
852 }
853 /**
854 * Ensure pivot records include created_at and updated_at timestamps.
855 *
856 * Adds timestamps when missing on insert and always sets updated_at. When
857 * invoked for updates, created_at is not overwritten if present.
858 *
859 * @param array $record The pivot attributes being inserted or updated
860 * @param bool $update Whether this is an update operation
861 *
862 * @return void
863 *
864 * @since 1.0.0
865 */
866 protected function add_timestamps_to_pivot(array &$record, $update = \false)
867 {
868 $timestamp = \date('Y-m-d H:i:s');
869 if (!$update && !isset($record['created_at'])) {
870 $record['created_at'] = $timestamp;
871 }
872 if (!isset($record['updated_at'])) {
873 $record['updated_at'] = $timestamp;
874 }
875 }
876 /**
877 * Get the local key for the relation.
878 *
879 * Returns the key on the parent model that is used to match related records.
880 *
881 * @return string The local key name
882 *
883 * @since 1.0.0
884 */
885 public function get_local_key()
886 {
887 return $this->parent_key;
888 }
889 }
890