1: <?php
2:
3: namespace Baril\Bonsai\Concerns;
4:
5: use Baril\Bonsai\Relations\BelongsToManyThroughClosures;
6: use Baril\Bonsai\Closure;
7: use Illuminate\Database\Eloquent\Builder;
8: use Illuminate\Database\Eloquent\Model;
9: use Illuminate\Support\Str;
10:
11: trait HasClosures
12: {
13: /**
14: * Return the name of the closure table.
15: *
16: * @return string
17: */
18: public function getClosureTable()
19: {
20: return property_exists($this, 'closureTable')
21: ? $this->closureTable
22: : Str::snake(class_basename($this)) . '_tree';
23: }
24:
25: /**
26: * Instanciate a new query builder on the closure table,
27: * optionally aliased.
28: *
29: * @param string|null $as
30: * @return \Illuminate\Database\Query\Builder
31: */
32: public function newClosureQuery($as = null)
33: {
34: return $this
35: ->newClosure(new static(), [], false)
36: ->newQuery()
37: ->when($as, function ($query, $as) {
38: $query->as($as);
39: });
40: }
41:
42: /**
43: * Create a new closure model instance.
44: *
45: * @see \Illuminate\Database\Eloquent\Model::newPivot()
46: *
47: * @param \Illuminate\Database\Eloquent\Model $parent
48: * @param array<string, mixed> $attributes
49: * @param bool $exists
50: * @param string|null $table
51: * @return \Baril\Bonsai\Closure
52: */
53: public function newClosure(Model $parent, array $attributes, $exists, $table = null)
54: {
55: return $this->newPivot($parent, $attributes, $table ?? $this->getClosureTable(), $exists, Closure::class);
56: }
57:
58: /**
59: * Define a belongs-to-many-through-closures relationship.
60: *
61: * @template TRelatedModel of \Illuminate\Database\Eloquent\Model
62: *
63: * @param class-string<TRelatedModel> $related
64: * @param string $table
65: * @param string $foreignPivotKey
66: * @param string $relatedPivotKey
67: * @param string $relationName
68: * @return \Baril\Bonsai\Relations\BelongsToManyThroughClosures<TRelatedModel, $this, \Illuminate\Database\Eloquent\Relations\Pivot>
69: */
70: protected function belongsToManyThroughClosures(
71: $related,
72: $table,
73: $foreignPivotKey = 'descendant_id',
74: $relatedPivotKey = 'ancestor_id',
75: $relationName = 'ancestors'
76: ) {
77: $instance = $this->newRelatedInstance($related);
78:
79: return (new BelongsToManyThroughClosures(
80: $instance->newQuery(),
81: $this,
82: $table,
83: $foreignPivotKey,
84: $relatedPivotKey,
85: $this->getKeyName(),
86: $instance->getKeyName(),
87: $relationName
88: ));
89: }
90:
91: /**
92: * Define a one-to-many relationship to the closure table.
93: *
94: * @template TRelatedModel of \Illuminate\Database\Eloquent\Model
95: *
96: * @param class-string<TRelatedModel> $related
97: * @param string $table
98: * @param string $foreignPivotKey
99: * @param string $relatedPivotKey
100: * @return \Illuminate\Database\Eloquent\Relations\HasMany<\Baril\Bonsai\Closure, $this>
101: */
102: protected function hasManyClosuresWith($class, $table, $foreignPivotKey = 'descendant_id', $relatedPivotKey = 'ancestor_id')
103: {
104: $instance = $this->newRelatedInstance($class);
105: $closure = $instance->newClosure($this, [], false, $table)
106: ->setPivotKeys($foreignPivotKey, $relatedPivotKey)
107: ->setRelatedModel($instance);
108:
109: return $this->newHasMany(
110: $closure->newQuery(),
111: $this,
112: $closure->qualifyColumn($foreignPivotKey),
113: $this->getKeyName()
114: );
115: }
116:
117: /**
118: * @param \Illuminate\Database\Eloquent\Builder $query
119: * @param string $relation
120: * @param int|null $depth
121: * @param callable|null $constraints
122: * @return void
123: */
124: protected function scopeWithManyThroughClosures(Builder $query, $relation, $depth = null, $constraints = null)
125: {
126: $query->with([$relation => function ($query) use ($depth, $constraints) {
127: if ($depth !== null) {
128: $query->maxDepth($depth)->orderByDepth();
129: }
130: if ($constraints !== null) {
131: $constraints($query);
132: }
133: }]);
134: }
135:
136: /**
137: * @param \Illuminate\Database\Eloquent\Builder $query
138: * @param mixed|\Illuminate\Database\Eloquent\Model $related
139: * @param string $relation
140: * @param string $scope
141: * @param int|null $maxDepth
142: * @param bool $withSelf
143: * @return void
144: */
145: protected function scopeWhereHasClosuresWith(Builder $query, $related, $relation, $scope, $maxDepth = null, $withSelf = false)
146: {
147: $relatedId = ($related instanceof Model) ? $related->getKey() : $related;
148:
149: $query->whereHas($relation, function ($query) use ($relatedId, $scope, $maxDepth, $withSelf) {
150: $query->$scope($relatedId)
151: ->when(null !== $maxDepth, function ($query) use ($maxDepth) {
152: $query->whereDepth('<=', $maxDepth);
153: })
154: ->when(!$withSelf, function ($query) {
155: $query->withoutSelf();
156: });
157: });
158: }
159: }
160: