Completed
Push — 4-cactus ( a8a64a...afff5c )
by Dante
20s queued 18s
created

PriorityBehavior::_getConditions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 2
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2020 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
namespace BEdita\Core\Model\Behavior;
14
15
use Cake\Datasource\EntityInterface;
16
use Cake\Event\Event;
17
use Cake\ORM\Behavior;
18
use Cake\ORM\Entity;
19
use Cake\Utility\Hash;
20
21
/**
22
 * Behavior to manage priorities.
23
 *
24
 * @since 4.0.0
25
 */
26
class PriorityBehavior extends Behavior
27
{
28
    /**
29
     * @inheritDoc
30
     */
31
    protected $_defaultConfig = [
32
        'fields' => [],
33
    ];
34
35
    /**
36
     * @inheritDoc
37
     */
38
    public function initialize(array $config): void
39
    {
40
        parent::initialize($config);
41
42
        $defaultConfig = (array)$this->getConfig('fields._all');
43
        $defaultConfig += [
44
            'scope' => false,
45
        ];
46
47
        $fields = Hash::normalize($this->getConfig('fields'));
0 ignored issues
show
Bug introduced by
It seems like $this->getConfig('fields') can also be of type null; however, parameter $data of Cake\Utility\Hash::normalize() 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

47
        $fields = Hash::normalize(/** @scrutinizer ignore-type */ $this->getConfig('fields'));
Loading history...
48
        unset($fields['_all']);
49
        foreach ($fields as $field => &$config) {
50
            $config = (array)$config + $defaultConfig;
51
        }
52
        unset($config);
53
54
        $this->setConfig('fields', $fields, false);
55
    }
56
57
    /**
58
     * Set up priorities before an entity is saved.
59
     * Use current max value + 1 if not set.
60
     * New values will start at 1.
61
     * Other priorities are shifted on priority change.
62
     *
63
     * @param \Cake\Event\Event $event Dispatched event.
64
     * @param \Cake\Datasource\EntityInterface $entity Entity instance.
65
     * @return void
66
     */
67
    public function beforeSave(Event $event, EntityInterface $entity)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed. ( Ignorable by Annotation )

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

67
    public function beforeSave(/** @scrutinizer ignore-unused */ Event $event, EntityInterface $entity)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
68
    {
69
        $fields = $this->getConfig('fields');
70
        foreach ($fields as $field => $config) {
71
            $this->updateEntityPriorities($entity, $field, $config);
72
        }
73
    }
74
75
    /**
76
     * Compact other priorities before the entity is deleted.
77
     *
78
     * @param \Cake\Event\Event $event Dispatched event.
79
     * @param \Cake\Datasource\EntityInterface $entity Entity instance.
80
     * @return void
81
     */
82
    public function beforeDelete(Event $event, EntityInterface $entity)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed. ( Ignorable by Annotation )

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

82
    public function beforeDelete(/** @scrutinizer ignore-unused */ Event $event, EntityInterface $entity)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
83
    {
84
        $fields = $this->getConfig('fields');
85
        foreach ($fields as $field => $config) {
86
            $this->compactEntityField($entity, $field, $config);
87
        }
88
    }
89
90
    /**
91
     * Compact entity field.
92
     *
93
     * @param EntityInterface $entity The entity
94
     * @param string $field The field
95
     * @param array $config The config
96
     * @return bool
97
     */
98
    public function compactEntityField(EntityInterface $entity, string $field, array $config): bool
99
    {
100
        if (empty($entity->get($field)) || empty($config['scope'])) {
101
            return false;
102
        }
103
104
        $conditions = $this->_getConditions($entity, $config['scope']);
105
        $this->compact($field, $entity->get($field), null, $conditions);
106
107
        return true;
108
    }
109
110
    /**
111
     * Update entity priorities.
112
     * Return true if data is updated, false otherwise.
113
     *
114
     * @param EntityInterface $entity The entity
115
     * @param string $field The field
116
     * @param array $config the config
117
     * @return bool
118
     */
119
    public function updateEntityPriorities(EntityInterface $entity, string $field, array $config): bool
120
    {
121
        if (empty($config['scope'])) {
122
            return false;
123
        }
124
125
        $conditions = $this->_getConditions($entity, $config['scope']);
126
        if (!empty($entity->get($field)) && $entity instanceof Entity) {
127
            $actualValue = $entity->get($field);
128
            $previousValue = $entity->getOriginal($field);
129
            if ($previousValue === $actualValue) {
130
                return false;
131
            }
132
133
            if ($previousValue < $actualValue) {
134
                $this->compact($field, $previousValue, $actualValue, $conditions);
135
136
                return true;
137
            }
138
            // $previousValue > $actualValue
139
            $this->expand($field, $actualValue, $previousValue, $conditions);
140
141
            return true;
142
        }
143
144
        $maxValue = $this->maxValue($field, $conditions);
145
        $entity->set($field, $maxValue + 1);
146
147
        return true;
148
    }
149
150
    /**
151
     * Get scope conditions from entity.
152
     *
153
     * @param \Cake\Datasource\EntityInterface $entity Entity instance.
154
     * @param string[] $scope A list of scope fields.
155
     * @return array A list of conditions.
156
     */
157
    protected function _getConditions(EntityInterface $entity, array $scope): array
158
    {
159
        $conditions = [];
160
        foreach ($scope as $item) {
161
            $keyField = sprintf('%s IS', $this->getTable()->aliasField($item));
162
            $conditions[$keyField] = $entity->get($item);
163
        }
164
165
        return $conditions;
166
    }
167
168
    /**
169
     * Create a gap in the priority list where an item can be inserted or moved.
170
     *
171
     * @param string $field Field name.
172
     * @param int $from The initial priority value to update.
173
     * @param int|null $to The final priority value to update.
174
     * @param array $conditions A list of scope conditions.
175
     * @return void
176
     */
177
    protected function expand(string $field, int $from, ?int $to = null, array $conditions = []): void
178
    {
179
        $conditions = $conditions + ["{$field} >=" => $from];
180
        if ($to !== null) {
181
            $conditions["{$field} <"] = $to;
182
        }
183
184
        $this->getTable()->updateAll([sprintf('%s = %1$s + 1', $field)], $conditions);
185
    }
186
187
    /**
188
     * Compact priority values.
189
     *
190
     * @param string $field Field name.
191
     * @param int $from The initial priority value to update.
192
     * @param int|null $to The final priority value to update.
193
     * @param array $conditions A list of scope conditions.
194
     * @return void
195
     */
196
    protected function compact(string $field, int $from, ?int $to = null, array $conditions = []): void
197
    {
198
        $conditions = $conditions + ["{$field} >" => $from];
199
        if ($to !== null) {
200
            $conditions["{$field} <="] = $to;
201
        }
202
203
        $this->getTable()->updateAll([sprintf('%s = %1$s - 1', $field)], $conditions);
204
    }
205
206
    /**
207
     * Get current max priority on an object relation
208
     *
209
     * @param string $field Priority field name.
210
     * @param array $conditions Scope conditions.
211
     * @return int
212
     */
213
    protected function maxValue(string $field, array $conditions): int
214
    {
215
        $query = $this->getTable()->find()->where($conditions);
216
        $query->select([
217
            'max_value' => $query->func()->max($this->getTable()->aliasField($field)),
218
        ]);
219
220
        return (int)Hash::get($query->toArray(), '0.max_value');
221
    }
222
}
223