SetValidator::keyForScope()   A
last analyzed

Complexity

Conditions 2
Paths 1

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 6
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 11
rs 10
1
<?php
2
3
namespace Encima\Albero;
4
5
use Illuminate\Database\Eloquent\Model;
6
7
class SetValidator
8
{
9
    /** @var \Illuminate\Database\Eloquent\Model */
10
    protected $node = null;
11
12
    public function __construct(Model $node)
13
    {
14
        $this->node = $node;
15
    }
16
17
    /**
18
     * Determine if the validation passes.
19
     *
20
     * @return boolean
21
     */
22
    public function passes(): bool
23
    {
24
        return
25
            $this->validateBounds() &&
26
            $this->validateDuplicates() &&
27
            $this->validateRoots();
28
    }
29
30
    /**
31
     * Determine if validation fails.
32
     *
33
     * @return boolean
34
     */
35
    public function fails(): bool
36
    {
37
        return !$this->passes();
38
    }
39
40
    /**
41
     * Validates bounds of the nested tree structure. It will perform checks on
42
     * the `lft`, `rgt` and `parent_id` columns. Mainly that they're not null,
43
     * rights greater than lefts, and that they're within the bounds of the parent.
44
     *
45
     * @return boolean
46
     */
47
    protected function validateBounds(): bool
48
    {
49
        $connection = $this->node->getConnection();
50
        $grammar = $connection->getQueryGrammar();
51
52
        $tableName = $this->node->getTable();
53
        $primaryKeyName = $this->node->getKeyName();
54
        $parentColumn = $this->node->getQualifiedParentColumnName();
55
56
        $lftCol = $grammar->wrap($this->node->getLeftColumnName());
0 ignored issues
show
Bug introduced by
It seems like $this->node->getLeftColumnName() can also be of type Illuminate\Database\Eloquent\Builder; however, parameter $value of Illuminate\Database\Query\Grammars\Grammar::wrap() does only seem to accept Illuminate\Database\Query\Expression|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

56
        $lftCol = $grammar->wrap(/** @scrutinizer ignore-type */ $this->node->getLeftColumnName());
Loading history...
57
        $rgtCol = $grammar->wrap($this->node->getRightColumnName());
58
59
        $qualifiedLftCol = $grammar->wrap($this->node->getQualifiedLeftColumnName());
60
        $qualifiedRgtCol = $grammar->wrap($this->node->getQualifiedRightColumnName());
61
        $qualifiedParentCol = $grammar->wrap($this->node->getQualifiedParentColumnName());
62
63
        $whereStm = "(${qualifiedLftCol} IS NULL OR
64
            ${qualifiedRgtCol} IS NULL OR
65
            ${qualifiedLftCol} >= ${qualifiedRgtCol} OR
66
            (${qualifiedParentCol} IS NOT NULL AND
67
            (${qualifiedLftCol} <= parent.${lftCol} OR
68
            ${qualifiedRgtCol} >= parent.${rgtCol})))";
69
70
        $query = $this->node->newQuery()
71
            ->join(
72
                $connection->raw($grammar->wrapTable($tableName).' AS parent'),
73
                $parentColumn,
74
                '=',
75
                $connection->raw('parent.'.$grammar->wrap($primaryKeyName)),
76
                'left outer'
77
            )
78
        ->whereRaw($whereStm);
79
80
        return ($query->count() == 0);
81
    }
82
83
    /**
84
     * Checks that there are no duplicates for the `lft` and `rgt` columns.
85
     *
86
     * @return boolean
87
     */
88
    protected function validateDuplicates(): bool
89
    {
90
        return (
91
            !$this->duplicatesExistForColumn($this->node->getQualifiedLeftColumnName()) &&
0 ignored issues
show
Bug introduced by
It seems like $this->node->getQualifiedLeftColumnName() can also be of type Illuminate\Database\Eloquent\Builder; however, parameter $column of Encima\Albero\SetValidat...licatesExistForColumn() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

91
            !$this->duplicatesExistForColumn(/** @scrutinizer ignore-type */ $this->node->getQualifiedLeftColumnName()) &&
Loading history...
92
            !$this->duplicatesExistForColumn($this->node->getQualifiedRightColumnName())
93
    );
94
    }
95
96
    /**
97
     * For each root of the whole nested set tree structure, checks that their
98
     * `lft` and `rgt` bounds are properly set.
99
     *
100
     * @return boolean
101
     */
102
    protected function validateRoots(): bool
103
    {
104
        $roots = forward_static_call([get_class($this->node), 'roots'])->get();
105
106
        // If a scope is defined in the model we should check that the roots are
107
        // valid *for each* value in the scope columns.
108
        if ($this->node->isScoped()) {
109
            return $this->validateRootsByScope($roots);
110
        }
111
112
        return $this->isEachRootValid($roots);
113
    }
114
115
    /**
116
     * Checks if duplicate values for the column specified exist. Takes
117
     * the Nested Set scope columns into account (if appropiate).
118
     *
119
     * @param   string  $column
120
     * @return  boolean
121
     */
122
    protected function duplicatesExistForColumn(string $column): bool
123
    {
124
        $connection = $this->node->getConnection();
125
        $grammar = $connection->getQueryGrammar();
126
127
        $columns = array_merge($this->node->getQualifiedScopedColumns(), [$column]);
0 ignored issues
show
Bug introduced by
It seems like $this->node->getQualifiedScopedColumns() can also be of type Illuminate\Database\Eloquent\Builder; however, parameter $array1 of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

127
        $columns = array_merge(/** @scrutinizer ignore-type */ $this->node->getQualifiedScopedColumns(), [$column]);
Loading history...
128
129
        $columnsForSelect = implode(', ', array_map(function ($col) use ($grammar) {
130
            return $grammar->wrap($col);
131
        }, $columns));
132
133
        $wrappedColumn = $grammar->wrap($column);
134
135
        $query = $this->node->newQuery()
136
            ->select($connection->raw("${columnsForSelect}, COUNT(${wrappedColumn})"))
137
            ->havingRaw("COUNT(${wrappedColumn}) > 1");
138
139
        foreach ($columns as $col) {
140
            $query->groupBy($col);
141
        }
142
143
        $result = $query->first();
144
145
        return !is_null($result);
146
    }
147
148
    /**
149
     * Check that each root node in the list supplied satisfies that its bounds
150
     * values (lft, rgt indexes) are less than the next.
151
     *
152
     * @param   mixed   $roots
153
     * @return  boolean
154
     */
155
    protected function isEachRootValid($roots): bool
156
    {
157
        $left = $right = 0;
158
159
        foreach ($roots as $root) {
160
            $rootLeft = $root->getLeft();
161
            $rootRight = $root->getRight();
162
163
            if (!($rootLeft > $left && $rootRight > $right)) {
164
                return false;
165
            }
166
167
            $left = $rootLeft;
168
            $right = $rootRight;
169
        }
170
171
        return true;
172
    }
173
174
    /**
175
     * Check that each root node in the list supplied satisfies that its bounds
176
     * values (lft, rgt indexes) are less than the next *within each scope*.
177
     *
178
     * @param   mixed   $roots
179
     * @return  boolean
180
     */
181
    protected function validateRootsByScope($roots): bool
182
    {
183
        foreach ($this->groupRootsByScope($roots) as $scope => $groupedRoots) {
184
            $valid = $this->isEachRootValid($groupedRoots);
185
186
            if (!$valid) {
187
                return false;
188
            }
189
        }
190
191
        return true;
192
    }
193
194
    /**
195
     * Given a list of root nodes, it returns an array in which the keys are the
196
     * array of the actual scope column values and the values are the root nodes
197
     * inside that scope themselves
198
     *
199
     * @param   mixed   $roots
200
     * @return  array
201
     */
202
    protected function groupRootsByScope($roots): array
203
    {
204
        $rootsGroupedByScope = [];
205
206
        foreach ($roots as $root) {
207
            $key = $this->keyForScope($root);
208
209
            if (!isset($rootsGroupedByScope[$key])) {
210
                $rootsGroupedByScope[$key] = [];
211
            }
212
213
            $rootsGroupedByScope[$key][] = $root;
214
        }
215
216
        return $rootsGroupedByScope;
217
    }
218
219
    /**
220
     * Builds a single string for the given scope columns values. Useful for
221
     * making array keys for grouping.
222
     *
223
     * @param Baum\Node   $node
0 ignored issues
show
Bug introduced by
The type Encima\Albero\Baum\Node was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
224
     * @return string
225
     */
226
    protected function keyForScope(Model $node): string
227
    {
228
        return implode('-', array_map(function ($column) use ($node) {
229
            $value = $node->getAttribute($column);
230
231
            if (is_null($value)) {
232
                return 'NULL';
233
            }
234
235
            return $value;
236
        }, $node->getScopedColumns()));
0 ignored issues
show
Bug introduced by
It seems like $node->getScopedColumns() can also be of type Illuminate\Database\Eloquent\Builder; however, parameter $arr1 of array_map() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

236
        }, /** @scrutinizer ignore-type */ $node->getScopedColumns()));
Loading history...
237
    }
238
}
239