Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.23% covered (warning)
87.23%
41 / 47
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
HasClosures
87.23% covered (warning)
87.23%
41 / 47
85.71% covered (warning)
85.71%
6 / 7
11.25
0.00% covered (danger)
0.00%
0 / 1
 getClosureTable
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 newClosureQuery
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 newClosure
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 belongsToManyThroughClosures
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 hasManyClosuresWith
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 scopeWithManyThroughClosures
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 scopeWhereHasClosuresWith
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace Baril\Bonsai\Concerns;
4
5use Baril\Bonsai\Relations\BelongsToManyThroughClosures;
6use Baril\Bonsai\Closure;
7use Illuminate\Database\Eloquent\Builder;
8use Illuminate\Database\Eloquent\Model;
9use Illuminate\Support\Str;
10
11trait 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}