Test Setup Failed
Push — master ( 94969c...9c343c )
by kicaj
02:34 queued 10s
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
cc 1
eloc 9
nc 1
nop 2
dl 0
loc 12
rs 9.9666
c 0
b 0
f 0
1
<?php
2
namespace 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 Slug\Exception\FieldException;
10
use Slug\Exception\FieldTypeException;
11
use Slug\Exception\IncrementException;
0 ignored issues
show
Bug introduced by
The type Slug\Exception\IncrementException 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...
12
use Slug\Exception\LengthException;
13
use Slug\Exception\LimitException;
14
use Slug\Exception\MethodException;
15
16
class SlugBehavior extends Behavior
17
{
18
19
    /**
20
     * Default config.
21
     *
22
     * @var array
23
     */
24
    public $defaultConfig = [
25
        'source' => 'name',
26
        'replacement' => '-',
27
        'finder' => 'list',
28
        'length' => 255,
29
    ];
30
31
    /**
32
     * {@inheritdoc}
33
     */
34
    public function initialize(array $config): void
35
    {
36
        parent::initialize($config);
37
38
        if (empty($this->getConfig())) {
39
            $this->setConfig('slug', $this->defaultConfig);
40
        }
41
42
        foreach ($this->getConfig() as $target => $config) {
43
            if (!is_array($config)) {
44
                $this->_configDelete($target);
45
46
                $target = $config;
47
48
                $this->setConfig($target, $this->defaultConfig);
49
            } else {
50
                $this->setConfig($target, array_merge($this->defaultConfig, $config));
51
            }
52
53
            if (!$this->getTable()->hasField($target)) {
54
                throw new FieldException(__d('slug', 'Cannot find target {0} field in schema.', $target));
55
            } elseif (!$this->getTable()->hasField($this->getConfig($target . '.source'))) {
0 ignored issues
show
Bug introduced by
It seems like $this->getConfig($target . '.source') can also be of type null; however, parameter $field of Cake\ORM\Table::hasField() 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

55
            } elseif (!$this->getTable()->hasField(/** @scrutinizer ignore-type */ $this->getConfig($target . '.source'))) {
Loading history...
56
                throw new FieldException(__d('slug', 'Cannot find source {0} field in schema.', $this->getConfig($target . '.source')));
57
            }
58
59
            if ($this->getTable()->getSchema()->getColumnType($target) !== 'string') {
60
                throw new FieldTypeException(__d('slug', 'Target field {0} should be string type.', $target));
61
            }
62
        }
63
    }
64
65
    /**
66
     * {@inheritdoc}
67
     */
68
    public function beforeFind(Event $event, Query $query, $options)
69
    {
70
        $query->select(array_keys($this->getConfig()));
0 ignored issues
show
Bug introduced by
It seems like $this->getConfig() can also be of type null; however, parameter $input of array_keys() 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

70
        $query->select(array_keys(/** @scrutinizer ignore-type */ $this->getConfig()));
Loading history...
71
    }
72
73
    /**
74
     * {@inheritdoc}
75
     */
76
    public function beforeSave(Event $event, EntityInterface $entity)
77
    {
78
        foreach ($this->getConfig() as $target => $config) {
79
            if (!isset($config['present']) || $config['present'] !== true) {
80
                $entity->{$target} = $this->createSlug($entity, $target);
81
            }
82
        }
83
    }
84
85
    /**
86
     * Create slug.
87
     *
88
     * @param EntityInterface $entity Entity.
89
     * @param string $target Target slug field name.
90
     * @return string Slug.
91
     */
92
    public function createSlug(EntityInterface $entity, string $target): string
93
    {
94
        $config = $this->getConfig($target);
95
96
        if ($entity->isDirty($config['source'])) {
97
            if ((mb_strlen($config['replacement']) + 1) < $config['length']) {
98
                if (isset($config['method'])) {
99
                    if (method_exists($this->getTable(), $config['method'])) {
100
                        $slug = $this->getTable()->{$config['method']}($entity->{$config['source']}, $config);
101
                    } else {
102
                        throw new MethodException(__d('slug', 'Method {0} does not exist.', $config['method']));
103
                    }
104
                } else {
105
                    $slug = Text::slug(mb_strtolower($entity->{$config['source']}), [
106
                        'replacement' => $config['replacement'],
107
                    ]);
108
                }
109
110
                $slugs = $this->sortSlugs($this->getSlugs($slug, $target));
111
112
                // Slug is just numbers
113
                if (preg_match('/^[0-9]+$/', $slug)) {
114
                    $numbers = preg_grep('/^[0-9]+$/', $slugs);
115
116
                    if (!empty($numbers)) {
117
                        sort($numbers);
118
119
                        $slug = end($numbers);
120
121
                        $slug++;
122
                    }
123
                }
124
125
                // Cut slug
126
                if (mb_strlen($replace = preg_replace('/\s+/', $config['replacement'], $slug)) > $config['length']) {
127
                    $slug = mb_substr($replace, 0, $config['length']);
128
129
                    // Update slug list based on cut slug
130
                    $slugs = $this->sortSlugs($this->getSlugs($slug, $field));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $field seems to be never defined.
Loading history...
131
                }
132
133
                $slug = preg_replace('/' . preg_quote($config['replacement']) . '$/', '', trim(mb_substr($slug, 0, $config['length'])));
134
135
                if (in_array($slug, $slugs)) {
136
                    $list = preg_grep('/^' . preg_replace('/' . preg_quote($config['replacement']) . '([1-9]{1}[0-9]*)$/', $config['replacement'], $slug) . '/', $slugs);
137
138
                    preg_match('/^(.*)' . preg_quote($config['replacement']) . '([1-9]{1}[0-9]*)$/', end($list), $matches);
139
140
                    if (empty($matches)) {
141
                        $increment = 1;
142
                    } else {
143
                        if (isset($matches[2])) {
144
                            $increment = $matches[2] += 1;
145
                        } else {
146
                            throw new IncrementException(__d('slug', 'Cannot create next suffix because matches are empty.'));
147
                        }
148
                    }
149
150
                    if (mb_strlen($slug . $config['replacement'] . $increment) <= $config['length']) {
151
                        $string = $slug;
152
                    } elseif (mb_strlen(mb_substr($slug, 0, -mb_strlen($increment))) + mb_strlen($config['replacement'] . $increment) <= $config['length']) {
153
                        $string = mb_substr($slug, 0, $config['length'] - mb_strlen($config['replacement'] . $increment));
154
                    } else {
155
                        $string = mb_substr($slug, 0, -(mb_strlen($config['replacement'] . $increment)));
156
                    }
157
158
                    if (mb_strlen($string) > 0) {
159
                        $slug = $string . $config['replacement'] . $increment;
160
161
                        // Refresh slugs list
162
                        $slugs = $this->sortSlugs(array_merge($slugs, $this->getSlugs($slug, $target)));
163
164
                        if (in_array($slug, $slugs)) {
165
                            return $this->createSlug($slug, $target);
0 ignored issues
show
Bug introduced by
$slug of type string is incompatible with the type Cake\Datasource\EntityInterface expected by parameter $entity of Slug\Model\Behavior\SlugBehavior::createSlug(). ( Ignorable by Annotation )

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

165
                            return $this->createSlug(/** @scrutinizer ignore-type */ $slug, $target);
Loading history...
166
                        }
167
                    } else {
168
                        throw new LengthException(__d('slug', 'Cannot create slug because there are no available names.'));
169
                    }
170
                }
171
172
                return $slug;
173
            } else {
174
                throw new LimitException(__d('slug', 'Limit of length in {0} field is too short.', $target));
175
            }
176
        } else {
177
            return $entity->{$target};
178
        }
179
    }
180
181
    /**
182
     * Get existing slug list.
183
     *
184
     * @param string $slug Slug to find.
185
     * @param string $target Target slug field name.
186
     * @return array List of slugs.
187
     */
188
    protected function getSlugs(string $slug, string $target): array
189
    {
190
        return $this->getTable()->find($this->getConfig($target . '.finder'), [
0 ignored issues
show
Bug introduced by
It seems like $this->getConfig($target . '.finder') can also be of type null; however, parameter $type of Cake\ORM\Table::find() 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

190
        return $this->getTable()->find(/** @scrutinizer ignore-type */ $this->getConfig($target . '.finder'), [
Loading history...
191
            'valueField' => $target,
192
        ])->where([
193
            'OR' => [
194
                $this->getTable()->getAlias() . '.' . $target => $slug,
195
                $this->getTable()->getAlias() . '.' . $target . ' REGEXP' => '^' . preg_replace('/' . preg_quote($this->getConfig($target . '.replacement')) . '([1-9]{1}[0-9]*)$/', '', $slug),
196
            ],
197
        ])->order([
198
            $this->getTable()->getAlias() . '.' . $target => 'ASC',
199
        ])->toArray();
200
    }
201
202
    /**
203
     * Sort slug list in normal mode.
204
     *
205
     * @param array $slugs Slug list.
206
     * @return array Slug list.
207
     */
208
    protected function sortSlugs(array $slugs): array
209
    {
210
        if (!empty($slugs)) {
211
            $slugs = array_unique($slugs);
212
213
            usort($slugs, function ($left, $right) {
214
                preg_match('/[1-9]{1}[0-9]*$/', $left, $matchLeft);
215
                preg_match('/[1-9]{1}[0-9]*$/', $right, $matchRight);
216
217
                return current($matchLeft) - current($matchRight);
218
            });
219
        }
220
221
        return $slugs;
222
    }
223
}
224