Passed
Push — 4-cactus ( 89e7fd...01e702 )
by Stefano
03:47 queued 28s
created

TreeBehavior::checkIntegrity()   B

Complexity

Conditions 5
Paths 16

Size

Total Lines 96
Code Lines 62

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 5
eloc 62
c 1
b 1
f 0
nc 16
nop 0
dl 0
loc 96
rs 8.5178

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2018 ChannelWeb Srl, Chialab Srl
5
 *
6
 * This file is part of BEdita: you can redistribute it and/or modify
7
 * it under the terms of the GNU Lesser General Public License as published
8
 * by the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
12
 */
13
14
namespace BEdita\Core\Model\Behavior;
15
16
use Cake\Database\Expression\Comparison;
17
use Cake\Database\Expression\IdentifierExpression;
18
use Cake\Database\Expression\QueryExpression;
19
use Cake\Database\Query;
20
use Cake\Datasource\EntityInterface;
21
use Cake\ORM\Behavior\TreeBehavior as CakeTreeBehavior;
22
use Cake\ORM\Table;
23
24
/**
25
 * This behavior adds absolute positioning of nodes on top of CakePHP {@see \Cake\ORM\Behavior\TreeBehavior}.
26
 *
27
 * {@inheritDoc}
28
 */
29
class TreeBehavior extends CakeTreeBehavior
30
{
31
    /**
32
     * {@inheritDoc}
33
     *
34
     * @codeCoverageIgnore
35
     */
36
    public function __construct(Table $table, array $config = [])
37
    {
38
        $this->_defaultConfig['implementedMethods'] += [
39
            'getCurrentPosition' => 'getCurrentPosition',
40
            'moveAt' => 'moveAt',
41
            'checkIntegrity' => 'checkIntegrity',
42
        ];
43
44
        parent::__construct($table, $config);
45
    }
46
47
    /**
48
     * Get current position of a node within its parent.
49
     *
50
     * @param \Cake\Datasource\EntityInterface $node Node to get position for.
51
     * @return int
52
     */
53
    public function getCurrentPosition(EntityInterface $node)
54
    {
55
        return $this->_scope($this->getTable()->find())
56
            ->where(function (QueryExpression $exp) use ($node) {
57
                $parentField = $this->getConfig('parent');
58
                $leftField = $this->getConfig('left');
59
60
                if (!$node->has($parentField)) {
61
                    $exp = $exp->isNull($this->getTable()->aliasField($parentField));
62
                } else {
63
                    $exp = $exp->eq($this->getTable()->aliasField($parentField), $node->get($parentField));
64
                }
65
66
                return $exp
67
                    ->lte($this->getTable()->aliasField($leftField), $node->get($leftField));
68
            })
69
            ->count();
70
    }
71
72
    /**
73
     * Move a node at a specific position without changing the parent.
74
     *
75
     * @param \Cake\Datasource\EntityInterface $node Node to be moved.
76
     * @param int|string $position New position. Can be either an integer, or a string (`'first'` or `'last'`).
77
     *      Negative integers are interpreted as number of positions from the end of the list. 0 (zero) is not allowed.
78
     * @return \Cake\Datasource\EntityInterface|false
79
     */
80
    public function moveAt(EntityInterface $node, $position)
81
    {
82
        return $this->getTable()->getConnection()->transactional(function () use ($node, $position) {
83
            $position = static::validatePosition($position);
84
            if ($position === false) {
85
                return false;
86
            }
87
88
            // ensure to use actual left and right fields
89
            unset($node[$this->getConfig('left')]);
90
            unset($node[$this->getConfig('right')]);
91
            $this->_ensureFields($node);
92
93
            $currentPosition = $this->getCurrentPosition($node);
94
            if ($position === $currentPosition) {
95
                // Do not perform extra queries. Position must still be normalized, so we'll need to re-check later.
96
                return $node;
97
            }
98
99
            $childrenCount = $this->_scope($this->getTable()->find())
100
                ->where(function (QueryExpression $exp) use ($node) {
101
                    $parentField = $this->getConfig('parent');
102
103
                    if (!$node->has($parentField)) {
104
                        return $exp->isNull($this->getTable()->aliasField($parentField));
105
                    }
106
107
                    return $exp->eq($this->getTable()->aliasField($parentField), $node->get($parentField));
108
                })
109
                ->count();
110
111
            // Normalize position. Transform negative indexes, and apply bounds.
112
            if ($position < 0) {
113
                $position = $childrenCount + $position + 1;
114
            }
115
            $position = max(1, min($position, $childrenCount));
116
117
            if ($position === $currentPosition) {
118
                // Already OK.
119
                return $node;
120
            }
121
122
            if ($position > $currentPosition) {
123
                return $this->moveDown($node, $position - $currentPosition);
124
            }
125
126
            return $this->moveUp($node, $currentPosition - $position);
127
        });
128
    }
129
130
    /**
131
     * Validate a position.
132
     *
133
     * @param int|string $position Position to be validated.
134
     * @return int|false
135
     */
136
    protected static function validatePosition($position)
137
    {
138
        if ($position === 'first') {
139
            return 1;
140
        }
141
        if ($position === 'last') {
142
            return -1;
143
        }
144
145
        $position = filter_var($position, FILTER_VALIDATE_INT);
146
        if ($position === false || $position === 0) {
147
            return false;
148
        }
149
150
        return $position;
151
    }
152
153
    /**
154
     * Calls $this->_recoverTree() without transactional(...)
155
     * Warning: you should set up a transactional flow manually if you use this method!
156
     *
157
     * @return void
158
     * @codeCoverageIgnore
159
     */
160
    public function nonAtomicRecover(): void
161
    {
162
        $this->_recoverTree();
163
    }
164
165
    /**
166
     * Run queries to check tree integrity.
167
     *
168
     * @return string[]
169
     */
170
    public function checkIntegrity(): array
171
    {
172
        $table = $this->getTable();
173
        $pk = $table->aliasField($table->getPrimaryKey());
0 ignored issues
show
Bug introduced by
It seems like $table->getPrimaryKey() can also be of type string[]; however, parameter $field of Cake\ORM\Table::aliasField() 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

173
        $pk = $table->aliasField(/** @scrutinizer ignore-type */ $table->getPrimaryKey());
Loading history...
174
        $left = $table->aliasField($this->getConfigOrFail('left'));
175
        $right = $table->aliasField($this->getConfigOrFail('right'));
176
        $parent = $table->aliasField($this->getConfigOrFail('parent'));
177
        $childAlias = sprintf('Child%s', $table->getAlias());
178
        $siblingAlias = sprintf('Sibling%s', $table->getAlias());
179
180
        $exists = function (Query $query): bool {
181
            return $query->select(['existing' => 1])->limit(1)->execute()->count() > 0;
182
        };
183
184
        $errors = [];
185
186
        // Check that for every record `lft < rght`.
187
        $query = $table->find()
188
            ->where(function (QueryExpression $exp) use ($left, $right): QueryExpression {
189
                return $exp->gte($left, new IdentifierExpression($right));
190
            });
191
        if ($exists($query)) {
192
            $errors[] = sprintf('Found record where %s >= %s', $this->getConfigOrFail('left'), $this->getConfigOrFail('right'));
193
        }
194
195
        // Check that for every parent, `parent.lft + 1 = MIN(children.lft)`
196
        $query = $table->find()
197
            ->innerJoin(
198
                [$childAlias => $table->getTable()],
199
                function (QueryExpression $exp) use ($pk, $childAlias): QueryExpression {
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } of type callable is incompatible with the type Cake\Database\ExpressionInterface|array|string expected by parameter $conditions of Cake\Database\Query::innerJoin(). ( Ignorable by Annotation )

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

199
                /** @scrutinizer ignore-type */ function (QueryExpression $exp) use ($pk, $childAlias): QueryExpression {
Loading history...
200
                    return $exp
201
                        ->equalFields($pk, sprintf('%s.%s', $childAlias, $this->getConfigOrFail('parent')));
202
                }
203
            )
204
            ->group([$pk, $left])
205
            ->having(function (QueryExpression $exp, Query $query) use ($childAlias, $left): QueryExpression {
206
                return $exp->notEq(
207
                    new Comparison($left, 1, null, '+'),
208
                    $query->func()->min(sprintf('%s.%s', $childAlias, $this->getConfigOrFail('left')))
209
                );
210
            });
211
        if ($exists($query)) {
212
            $errors[] = sprintf('Found record where parent.%s + 1 != MIN(children.%1$s)', $this->getConfigOrFail('left'));
213
        }
214
215
        // Check that for every parent, `parent.rght - 1 = MAX(children.rght)`
216
        $query = $table->find()
217
            ->innerJoin(
218
                [$childAlias => $table->getTable()],
219
                function (QueryExpression $exp) use ($pk, $childAlias): QueryExpression {
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } of type callable is incompatible with the type Cake\Database\ExpressionInterface|array|string expected by parameter $conditions of Cake\Database\Query::innerJoin(). ( Ignorable by Annotation )

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

219
                /** @scrutinizer ignore-type */ function (QueryExpression $exp) use ($pk, $childAlias): QueryExpression {
Loading history...
220
                    return $exp
221
                        ->equalFields($pk, sprintf('%s.%s', $childAlias, $this->getConfigOrFail('parent')));
222
                }
223
            )
224
            ->group([$pk, $right])
225
            ->having(function (QueryExpression $exp, Query $query) use ($childAlias, $right): QueryExpression {
226
                return $exp->notEq(
227
                    new Comparison($right, 1, null, '-'),
228
                    $query->func()->max(sprintf('%s.%s', $childAlias, $this->getConfigOrFail('right')))
229
                );
230
            });
231
        if ($exists($query)) {
232
            $errors[] = sprintf('Found record where parent.%s - 1 != MAX(children.%1$s)', $this->getConfigOrFail('right'));
233
        }
234
235
        // Check that for every node, `node.lft - 1 = MAX(sibling.rght)` where `sibling.lft <= node.lft`.
236
        $query = $table->find()
237
            ->innerJoin(
238
                [$siblingAlias => $table->getTable()],
239
                function (QueryExpression $exp) use ($table, $left, $parent, $pk, $siblingAlias): QueryExpression {
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } of type callable is incompatible with the type Cake\Database\ExpressionInterface|array|string expected by parameter $conditions of Cake\Database\Query::innerJoin(). ( Ignorable by Annotation )

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

239
                /** @scrutinizer ignore-type */ function (QueryExpression $exp) use ($table, $left, $parent, $pk, $siblingAlias): QueryExpression {
Loading history...
240
                    return $exp
241
                        ->add($exp->or(function (QueryExpression $exp) use ($parent, $siblingAlias): QueryExpression {
242
                            $siblingParent = sprintf('%s.%s', $siblingAlias, $this->getConfigOrFail('parent'));
243
244
                            return $exp
245
                                ->equalFields($parent, $siblingParent)
246
                                ->add($exp->and(function (QueryExpression $exp) use ($parent, $siblingParent): QueryExpression {
247
                                    return $exp->isNull($parent)->isNull($siblingParent);
248
                                }));
249
                        }))
250
                        ->gte($left, new IdentifierExpression(sprintf('%s.%s', $siblingAlias, $this->getConfigOrFail('left'))))
251
                        ->notEq($pk, new IdentifierExpression(sprintf('%s.%s', $siblingAlias, $table->getPrimaryKey())));
0 ignored issues
show
Bug introduced by
It seems like $table->getPrimaryKey() can also be of type string[]; however, parameter $values of sprintf() does only seem to accept double|integer|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

251
                        ->notEq($pk, new IdentifierExpression(sprintf('%s.%s', $siblingAlias, /** @scrutinizer ignore-type */ $table->getPrimaryKey())));
Loading history...
252
                }
253
            )
254
            ->group([$pk, $left])
255
            ->having(function (QueryExpression $exp, Query $query) use ($siblingAlias, $left): QueryExpression {
256
                return $exp->notEq(
257
                    new Comparison($left, 1, null, '-'),
258
                    $query->func()->max(sprintf('%s.%s', $siblingAlias, $this->getConfigOrFail('right')))
259
                );
260
            });
261
        if ($exists($query)) {
262
            $errors[] = sprintf('Found record where %s - 1 != MAX(previousSiblings.%s)', $this->getConfigOrFail('left'), $this->getConfigOrFail('right'));
263
        }
264
265
        return $errors;
266
    }
267
}
268