Passed
Push — master ( a5567a...af0334 )
by kicaj
09:06
created

SlugBehavior::getSlugs()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 12
rs 9.9666
c 0
b 0
f 0
cc 1
nc 1
nop 2
1
<?php
2
namespace SlicesCake\Slug\Model\Behavior;
3
4
use Cake\Datasource\EntityInterface;
5
use Cake\Event\Event;
6
use Cake\ORM\Behavior;
7
use Cake\ORM\Query;
8
use Cake\Utility\Text;
9
use SlicesCake\Slug\Exception\FieldException;
10
use SlicesCake\Slug\Exception\FieldTypeException;
11
use SlicesCake\Slug\Exception\IncrementException;
12
use SlicesCake\Slug\Exception\LengthException;
13
use SlicesCake\Slug\Exception\LimitException;
14
use SlicesCake\Slug\Exception\MethodException;
15
16
/**
17
 * SlugBehavior
18
 */
19
class SlugBehavior extends Behavior
20
{
21
22
    /**
23
     * Default config.
24
     *
25
     * @var array
26
     */
27
    public array $defaultConfig = [
28
        'source' => 'name',
29
        'replacement' => '-',
30
        'finder' => 'list',
31
        'length' => 255,
32
    ];
33
34
    /**
35
     * {@inheritdoc}
36
     */
37
    public function initialize(array $config): void
38
    {
39
        parent::initialize($config);
40
41
        if (empty($this->getConfig())) {
42
            $this->setConfig('slug', $this->defaultConfig);
43
        }
44
45
        foreach ($this->getConfig() as $target => $config) {
46
            if (!is_array($config)) {
47
                $this->_configDelete($target);
48
49
                $target = $config;
50
51
                $this->setConfig($target, $this->defaultConfig);
52
            } else {
53
                $this->setConfig($target, array_merge($this->defaultConfig, $config));
54
            }
55
56
            if (!$this->table()->hasField($target)) {
57
                throw new FieldException(__d('slug', 'Cannot find target {0} field in schema.', $target));
58
            } elseif (!$this->table()->hasField($this->getConfig($target . '.source'))) {
59
                throw new FieldException(__d('slug', 'Cannot find source {0} field in schema.', $this->getConfig($target . '.source')));
60
            }
61
62
            if ($this->table()->getSchema()->getColumnType($target) !== 'string') {
63
                throw new FieldTypeException(__d('slug', 'Target field {0} should be string type.', $target));
64
            }
65
        }
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71
    public function beforeFind(Event $event, Query $query, $options)
72
    {
73
        $config = $this->getConfig();
74
75
        if (is_array($config)) {
76
            $query->select(array_keys($config));
77
        }
78
    }
79
80
    /**
81
     * {@inheritdoc}
82
     */
83
    public function beforeSave(Event $event, EntityInterface $entity)
84
    {
85
        foreach ($this->getConfig() as $target => $config) {
86
            if (!isset($config['present']) || $config['present'] !== true) {
87
                if ($entity->isAccessible($target)) {
88
                    $entity->{$target} = $this->createSlug($entity, $target);
89
                }
90
            }
91
        }
92
    }
93
94
    /**
95
     * Create slug.
96
     *
97
     * @param EntityInterface|string $entity Entity.
98
     * @param string $target Target slug field name.
99
     * @return string Slug.
100
     */
101
    public function createSlug($entity, string $target): string
102
    {
103
        $config = $this->getConfig($target);
104
105
        if ($entity->isDirty($config['source']) || empty($entity->{$target})) {
106
            if ((mb_strlen($config['replacement']) + 1) < $config['length']) {
107
                if (isset($config['method'])) {
108
                    if (method_exists($this->table(), $config['method'])) {
109
                        $slug = $this->table()->{$config['method']}($entity, $config);
110
                    } else {
111
                        throw new MethodException(__d('slug', 'Method {0} does not exist.', $config['method']));
112
                    }
113
                } else {
114
                    $slug = Text::slug(mb_strtolower($entity->{$config['source']}), [
115
                        'replacement' => $config['replacement'],
116
                    ]);
117
                }
118
119
                $slugs = $this->sortSlugs($this->getSlugs($slug, $target));
120
121
                // Slug is just numbers
122
                if (preg_match('/^[0-9]+$/', $slug)) {
123
                    $numbers = preg_grep('/^[0-9]+$/', $slugs);
124
125
                    if (!empty($numbers)) {
126
                        sort($numbers);
127
128
                        $slug = end($numbers);
129
130
                        $slug++;
131
                    }
132
                }
133
134
                // Cut slug
135
                if (mb_strlen($replace = preg_replace('/\s+/', $config['replacement'], $slug)) > $config['length']) {
136
                    $slug = mb_substr($replace, 0, $config['length']);
137
138
                    // Update slug list based on cut slug
139
                    $slugs = $this->sortSlugs($this->getSlugs($slug, $target));
140
                }
141
142
                $slug = preg_replace('/' . preg_quote($config['replacement']) . '$/', '', trim(mb_substr($slug, 0, $config['length'])));
143
144
                if (in_array($slug, $slugs)) {
145
                    $list = preg_grep('/^' . preg_replace('/' . preg_quote($config['replacement']) . '([1-9]{1}[0-9]*)$/', $config['replacement'], $slug) . '/', $slugs);
146
147
                    preg_match('/^(.*)' . preg_quote($config['replacement']) . '([1-9]{1}[0-9]*)$/', end($list), $matches);
148
149
                    if (empty($matches)) {
150
                        $increment = 1;
151
                    } else {
152
                        if (isset($matches[2])) {
153
                            $increment = $matches[2] += 1;
154
                        } else {
155
                            throw new IncrementException(__d('slug', 'Cannot create next suffix because matches are empty.'));
156
                        }
157
                    }
158
159
                    if (mb_strlen($slug . $config['replacement'] . $increment) <= $config['length']) {
160
                        $string = $slug;
161
                    } elseif (mb_strlen(mb_substr($slug, 0, -mb_strlen($increment))) + mb_strlen($config['replacement'] . $increment) <= $config['length']) {
162
                        $string = mb_substr($slug, 0, $config['length'] - mb_strlen($config['replacement'] . $increment));
163
                    } else {
164
                        $string = mb_substr($slug, 0, -(mb_strlen($config['replacement'] . $increment)));
165
                    }
166
167
                    if (mb_strlen($string) > 0) {
168
                        $slug = $string . $config['replacement'] . $increment;
169
170
                        // Refresh slugs list
171
                        $slugs = $this->sortSlugs(array_merge($slugs, $this->getSlugs($slug, $target)));
172
173
                        if (in_array($slug, $slugs)) {
174
                            return $this->createSlug($slug, $target);
175
                        }
176
                    } else {
177
                        throw new LengthException(__d('slug', 'Cannot create slug because there are no available names.'));
178
                    }
179
                }
180
181
                return $slug;
182
            } else {
183
                throw new LimitException(__d('slug', 'Limit of length in {0} field is too short.', $target));
184
            }
185
        } else {
186
            return $entity->{$target};
187
        }
188
    }
189
190
    /**
191
     * Get existing slug list.
192
     *
193
     * @param string $slug Slug to find.
194
     * @param string $target Target slug field name.
195
     * @return array List of slugs.
196
     */
197
    protected function getSlugs(string $slug, string $target): array
198
    {
199
        return $this->table()->find($this->getConfig($target . '.finder'), [
200
            'valueField' => $target,
201
        ])->where([
202
            'OR' => [
203
                $this->table()->getAlias() . '.' . $target => $slug,
204
                $this->table()->getAlias() . '.' . $target . ' REGEXP' => '^' . preg_replace('/' . preg_quote($this->getConfig($target . '.replacement')) . '([1-9]{1}[0-9]*)$/', '', $slug),
205
            ],
206
        ])->order([
207
            $this->table()->getAlias() . '.' . $target => 'ASC',
208
        ])->toArray();
209
    }
210
211
    /**
212
     * Sort slug list in normal mode.
213
     *
214
     * @param array $slugs Slug list.
215
     * @return array Slug list.
216
     */
217
    protected function sortSlugs(array $slugs): array
218
    {
219
        if (!empty($slugs)) {
220
            $slugs = array_unique($slugs);
221
222
            usort($slugs, function ($left, $right) {
223
                preg_match('/[1-9]{1}[0-9]*$/', $left, $matchLeft);
224
                preg_match('/[1-9]{1}[0-9]*$/', $right, $matchRight);
225
226
                return current($matchLeft) - current($matchRight);
227
            });
228
        }
229
230
        return $slugs;
231
    }
232
}
233