Passed
Push — 4-cactus ( 90f718...103fed )
by Stefano
07:48 queued 05:08
created

TreeBehavior   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 117
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 46
dl 0
loc 117
rs 10
c 0
b 0
f 0
wmc 15

4 Methods

Rating   Name   Duplication   Size   Complexity  
B moveAt() 0 42 7
A validatePosition() 0 15 5
A __construct() 0 8 1
A getCurrentPosition() 0 17 2
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\QueryExpression;
17
use Cake\Datasource\EntityInterface;
18
use Cake\ORM\Behavior\TreeBehavior as CakeTreeBehavior;
19
use Cake\ORM\Table;
20
21
/**
22
 * This behavior adds absolute positioning of nodes on top of CakePHP {@see \Cake\ORM\Behavior\TreeBehavior}.
23
 *
24
 * {@inheritDoc}
25
 */
26
class TreeBehavior extends CakeTreeBehavior
27
{
28
29
    /**
30
     * {@inheritDoc}
31
     *
32
     * @codeCoverageIgnore
33
     */
34
    public function __construct(Table $table, array $config = [])
35
    {
36
        $this->_defaultConfig['implementedMethods'] += [
37
            'getCurrentPosition' => 'getCurrentPosition',
38
            'moveAt' => 'moveAt',
39
        ];
40
41
        parent::__construct($table, $config);
42
    }
43
44
    /**
45
     * Get current position of a node within its parent.
46
     *
47
     * @param \Cake\Datasource\EntityInterface $node Node to get position for.
48
     * @return int
49
     */
50
    public function getCurrentPosition(EntityInterface $node)
51
    {
52
        return $this->_scope($this->getTable()->find())
53
            ->where(function (QueryExpression $exp) use ($node) {
54
                $parentField = $this->getConfig('parent');
55
                $leftField = $this->getConfig('left');
56
57
                if (!$node->has($parentField)) {
58
                    $exp = $exp->isNull($this->getTable()->aliasField($parentField));
59
                } else {
60
                    $exp = $exp->eq($this->getTable()->aliasField($parentField), $node->get($parentField));
61
                }
62
63
                return $exp
64
                    ->lte($this->getTable()->aliasField($leftField), $node->get($leftField));
65
            })
66
            ->count();
67
    }
68
69
    /**
70
     * Move a node at a specific position without changing the parent.
71
     *
72
     * @param \Cake\Datasource\EntityInterface $node Node to be moved.
73
     * @param int|string $position New position. Can be either an integer, or a string (`'first'` or `'last'`).
74
     *      Negative integers are interpreted as number of positions from the end of the list. 0 (zero) is not allowed.
75
     * @return \Cake\Datasource\EntityInterface|false
76
     */
77
    public function moveAt(EntityInterface $node, $position)
78
    {
79
        return $this->getTable()->getConnection()->transactional(function () use ($node, $position) {
80
            $position = static::validatePosition($position);
81
            if ($position === false) {
82
                return false;
83
            }
84
85
            $currentPosition = $this->getCurrentPosition($node);
86
            if ($position === $currentPosition) {
87
                // Do not perform extra queries. Position must still be normalized, so we'll need to re-check later.
88
                return $node;
89
            }
90
91
            $childrenCount = $this->_scope($this->getTable()->find())
92
                ->where(function (QueryExpression $exp) use ($node) {
93
                    $parentField = $this->getConfig('parent');
94
95
                    if (!$node->has($parentField)) {
96
                        return $exp->isNull($this->getTable()->aliasField($parentField));
97
                    }
98
99
                    return $exp->eq($this->getTable()->aliasField($parentField), $node->get($parentField));
100
                })
101
                ->count();
102
103
            // Normalize position. Transform negative indexes, and apply bounds.
104
            if ($position < 0) {
105
                $position = $childrenCount + $position + 1;
106
            }
107
            $position = max(1, min($position, $childrenCount));
108
109
            if ($position === $currentPosition) {
110
                // Already OK.
111
                return $node;
112
            }
113
114
            if ($position > $currentPosition) {
115
                return $this->moveDown($node, $position - $currentPosition);
116
            }
117
118
            return $this->moveUp($node, $currentPosition - $position);
119
        });
120
    }
121
122
    /**
123
     * Validate a position.
124
     *
125
     * @param int|string $position Position to be validated.
126
     * @return int|false
127
     */
128
    protected static function validatePosition($position)
129
    {
130
        if ($position === 'first') {
131
            return 1;
132
        }
133
        if ($position === 'last') {
134
            return -1;
135
        }
136
137
        $position = filter_var($position, FILTER_VALIDATE_INT);
138
        if ($position === false || $position === 0) {
139
            return false;
140
        }
141
142
        return $position;
143
    }
144
}
145