Passed
Push — master ( 1ba282...6bca00 )
by Andreas
14:46
created

generate_unique_name()   B

Complexity

Conditions 8
Paths 11

Size

Total Lines 62
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 9

Importance

Changes 0
Metric Value
cc 8
eloc 33
nc 11
nop 2
dl 0
loc 62
ccs 24
cts 32
cp 0.75
crap 9
rs 8.1475
c 0
b 0
f 0

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
 * @package midcom.helper.reflector
4
 * @author CONTENT CONTROL http://www.contentcontrol-berlin.de/
5
 * @copyright CONTENT CONTROL http://www.contentcontrol-berlin.de/
6
 * @license http://www.gnu.org/licenses/gpl.html GNU General Public License
7
 */
8
9
/**
10
 * Helper class for object name handling
11
 *
12
 * @package midcom.helper.reflector
13
 */
14
class midcom_helper_reflector_nameresolver
15
{
16
    /**
17
     * The object we're working with
18
     *
19
     * @var midcom_core_dbaobject
20
     */
21
    private $_object;
22
23 148
    public function __construct($object)
24
    {
25 148
        $this->_object = $object;
26 148
    }
27
28
    /**
29
     * Resolves the "name" of given object
30
     *
31
     * @param string $name_property property to use as "name", if left to default (null), will be reflected
32
     * @return string value of name property or boolean false on failure
33
     */
34 148
    public function get_object_name($name_property = null)
35
    {
36 148
        if ($name_property === null) {
37 148
            $name_property = midcom_helper_reflector::get_name_property($this->_object);
38
        }
39 148
        if (    empty($name_property)
40 148
            || !midcom_helper_reflector::get($this->_object)->property_exists($name_property)) {
41
            // Could not resolve valid property
42 1
            return false;
43
        }
44
        // Make copy via typecast, very important or we might accidentally manipulate the given object
45 148
        return (string)$this->_object->{$name_property};
46
    }
47
48
    /**
49
     * Checks for "clean" URL name
50
     *
51
     * @see http://trac.midgard-project.org/ticket/809
52
     * @param string $name_property property to use as "name", if left to default (null), will be reflected
53
     */
54 1
    public function name_is_clean($name_property = null) : bool
55
    {
56 1
        $name_copy = $this->get_object_name($name_property);
57 1
        if (empty($name_copy)) {
58
            // empty name is not "clean"
59
            return false;
60
        }
61 1
        return $name_copy === midcom_helper_misc::urlize($name_copy);
62
    }
63
64
    /**
65
     * Checks for URL-safe name
66
     *
67
     * @see http://trac.midgard-project.org/ticket/809
68
     * @param string $name_property property to use as "name", if left to default (null), will be reflected
69
     */
70 91
    public function name_is_safe($name_property = null) : bool
71
    {
72 91
        $name_copy = $this->get_object_name($name_property);
73
74 91
        if (empty($name_copy)) {
75
            // empty name is not url-safe
76
            return false;
77
        }
78 91
        return $name_copy === rawurlencode($name_copy);
79
    }
80
81
    /**
82
     * Checks for URL-safe name, this variant accepts empty name
83
     *
84
     * @see http://trac.midgard-project.org/ticket/809
85
     * @param string $name_property property to use as "name", if left to default (null), will be reflected
86
     */
87 144
    public function name_is_safe_or_empty($name_property = null) : bool
88
    {
89 144
        $name_copy = $this->get_object_name($name_property);
90 144
        if ($name_copy === false) {
0 ignored issues
show
introduced by
The condition $name_copy === false is always false.
Loading history...
91
            //get_object_name failed
92
            return false;
93
        }
94 144
        if (empty($name_copy)) {
95 71
            return true;
96
        }
97 90
        return $this->name_is_safe($name_property);
98
    }
99
100
    /**
101
     * Checks for "clean" URL name, this variant accepts empty name
102
     *
103
     * @see http://trac.midgard-project.org/ticket/809
104
     * @param string $name_property property to use as "name", if left to default (null), will be reflected
105
     */
106
    public function name_is_clean_or_empty($name_property = null) : bool
107
    {
108
        $name_copy = $this->get_object_name($name_property);
109
        if ($name_copy === false) {
0 ignored issues
show
introduced by
The condition $name_copy === false is always false.
Loading history...
110
            //get_object_name failed
111
            return false;
112
        }
113
        if (empty($name_copy)) {
114
            return true;
115
        }
116
        return $this->name_is_clean($name_property);
117
    }
118
119
    /**
120
     * Check that none of given objects siblings have same name, or the name is empty.
121
     */
122 144
    public function name_is_unique_or_empty() : bool
123
    {
124 144
        $name_copy = $this->get_object_name();
125 144
        if (   empty($name_copy)
126 144
            && $name_copy !== false) {
127
            // Allow empty string name
128 71
            return true;
129
        }
130 90
        return $this->name_is_unique();
131
    }
132
133
    /**
134
     * Check that none of given object's siblings have same name.
135
     */
136 90
    public function name_is_unique() : bool
137
    {
138
        // Get current name and sanity-check
139 90
        if (empty($this->get_object_name())) {
140
            // We do not check for empty names, and do not consider them to be unique
141 4
            return false;
142
        }
143
144
        // Start the magic
145 90
        midcom::get()->auth->request_sudo('midcom.helper.reflector');
146 90
        $parent = midcom_helper_reflector_tree::get_parent($this->_object);
0 ignored issues
show
Bug introduced by
$this->_object of type midcom_core_dbaobject is incompatible with the type midgard\portable\api\mgdobject expected by parameter $object of midcom_helper_reflector_tree::get_parent(). ( Ignorable by Annotation )

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

146
        $parent = midcom_helper_reflector_tree::get_parent(/** @scrutinizer ignore-type */ $this->_object);
Loading history...
147 90
        if (!empty($parent->guid)) {
148
            // We have parent, check siblings
149 86
            $parent_resolver = new midcom_helper_reflector_tree($parent);
150 86
            $sibling_classes = $parent_resolver->get_child_classes();
151 86
            if (!in_array('midgard_attachment', $sibling_classes)) {
152 86
                $sibling_classes[] = 'midgard_attachment';
153
            }
154 86
            if (!$this->_name_is_unique_check_siblings($sibling_classes, $parent)) {
155 1
                midcom::get()->auth->drop_sudo();
156 86
                return false;
157
            }
158
        } else {
159
            // No parent, we might be a root level class
160 9
            $is_root_class = false;
161 9
            $root_classes = midcom_helper_reflector_tree::get_root_classes();
162 9
            foreach ($root_classes as $classname) {
163 9
                if (midcom::get()->dbfactory->is_a($this->_object, $classname)) {
164 5
                    $is_root_class = true;
165 5
                    if (!$this->_name_is_unique_check_roots($root_classes)) {
166
                        midcom::get()->auth->drop_sudo();
167 9
                        return false;
168
                    }
169
                }
170
            }
171 9
            if (!$is_root_class) {
172
                // This should not happen, logging error and returning true (even though it's potentially dangerous)
173 4
                midcom::get()->auth->drop_sudo();
174 4
                debug_add("Object " . get_class($this->_object) . " #" . $this->_object->id . " has no valid parent but is not listed in the root classes, don't know what to do, returning true and supposing user knows what he is doing", MIDCOM_LOG_ERROR);
175 4
                return true;
176
            }
177
        }
178
179 87
        midcom::get()->auth->drop_sudo();
180
        // If we get this far we know we don't have name clashes
181 87
        return true;
182
    }
183
184
    /**
185
     * Check uniqueness for each sibling
186
     */
187 86
    private function _name_is_unique_check_siblings(array $sibling_classes, $parent) : bool
188
    {
189 86
        $name_copy = $this->get_object_name();
190
191 86
        foreach ($sibling_classes as $schema_type) {
192 86
            $qb = $this->_get_sibling_qb($schema_type, $parent);
193 86
            if (!$qb) {
194 86
                continue;
195
            }
196 86
            $child_name_property = midcom_helper_reflector::get_name_property(new $schema_type);
197
198 86
            $qb->add_constraint($child_name_property, '=', $name_copy);
199 86
            if ($qb->count()) {
200 1
                debug_add("Name clash in sibling class {$schema_type} for " . get_class($this->_object) . " #{$this->_object->id} (path '" . midcom_helper_reflector_tree::resolve_path($this->_object, '/') . "')" );
0 ignored issues
show
Bug introduced by
$this->_object of type midcom_core_dbaobject is incompatible with the type midgard\portable\api\mgdobject expected by parameter $object of midcom_helper_reflector_tree::resolve_path(). ( Ignorable by Annotation )

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

200
                debug_add("Name clash in sibling class {$schema_type} for " . get_class($this->_object) . " #{$this->_object->id} (path '" . midcom_helper_reflector_tree::resolve_path(/** @scrutinizer ignore-type */ $this->_object, '/') . "')" );
Loading history...
201 86
                return false;
202
            }
203
        }
204 86
        return true;
205
    }
206
207
    /**
208
     * Check uniqueness for each root level class
209
     */
210 5
    private function _name_is_unique_check_roots(array $sibling_classes) : bool
211
    {
212 5
        $name_copy = $this->get_object_name();
213
214 5
        foreach ($sibling_classes as $schema_type) {
215 5
            $qb = $this->_get_root_qb($schema_type);
216 5
            if (!$qb) {
217 5
                continue;
218
            }
219 5
            $child_name_property = midcom_helper_reflector::get_name_property(new $schema_type);
220
221 5
            $qb->add_constraint($child_name_property, '=', $name_copy);
222 5
            if ($qb->count()) {
223
                debug_add("Name clash in sibling class {$schema_type} for " . get_class($this->_object) . " #{$this->_object->id} (path '" . midcom_helper_reflector_tree::resolve_path($this->_object, '/') . "')" );
0 ignored issues
show
Bug introduced by
$this->_object of type midcom_core_dbaobject is incompatible with the type midgard\portable\api\mgdobject expected by parameter $object of midcom_helper_reflector_tree::resolve_path(). ( Ignorable by Annotation )

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

223
                debug_add("Name clash in sibling class {$schema_type} for " . get_class($this->_object) . " #{$this->_object->id} (path '" . midcom_helper_reflector_tree::resolve_path(/** @scrutinizer ignore-type */ $this->_object, '/') . "')" );
Loading history...
224 5
                return false;
225
            }
226
        }
227 5
        return true;
228
    }
229
230
    /**
231
     * Generates an unique name for the given object.
232
     *
233
     * 1st IF name is empty, we generate one from title (if title is empty too, we return false)
234
     * Then we check if it's unique, if not we add an incrementing
235
     * number to it (before this we make some educated guesses about a
236
     * good starting value)
237
     *
238
     * @param string $title_property Property of the object to use at title, if null will be reflected (see midcom_helper_reflector::get_object_title())
239
     * @param string $extension The file extension, when working with attachments
240
     * @return string string usable as name or boolean false on critical failures
241
     */
242 76
    public function generate_unique_name($title_property = null, $extension = '')
243
    {
244
        // Get current name and sanity-check
245 76
        $original_name = $this->get_object_name();
246 76
        if ($original_name === false) {
0 ignored issues
show
introduced by
The condition $original_name === false is always false.
Loading history...
247
            // Fatal error with name resolution
248
            debug_add("Object " . get_class($this->_object) . " #{$this->_object->id} returned critical failure for name resolution, aborting", MIDCOM_LOG_WARN);
249
            return false;
250
        }
251
252
        // We need the name of the "name" property later
253 76
        $name_prop = midcom_helper_reflector::get_name_property($this->_object);
254
255 76
        if (!empty($original_name)) {
256
            $current_name = (string)$original_name;
257
        } else {
258
            // Empty name, try to generate from title
259 76
            $title_copy = midcom_helper_reflector::get_object_title($this->_object, $title_property);
260 76
            if ($title_copy === false) {
0 ignored issues
show
introduced by
The condition $title_copy === false is always false.
Loading history...
261
                // Fatal error with title resolution
262
                debug_add("Object " . get_class($this->_object) . " #{$this->_object->id} returned critical failure for title resolution when name was empty, aborting", MIDCOM_LOG_WARN);
263
                return false;
264
            }
265 76
            if (empty($title_copy)) {
266 72
                debug_add("Object " . get_class($this->_object) . " #{$this->_object->id} has empty name and title, aborting", MIDCOM_LOG_WARN);
267 72
                return false;
268
            }
269 8
            $current_name = midcom_helper_misc::urlize($title_copy);
270 8
            unset($title_copy);
271
        }
272
273
        // incrementer, the number to add as suffix and the base name. see _generate_unique_name_resolve_i()
274 8
        list($i, $base_name) = $this->_generate_unique_name_resolve_i($current_name, $extension);
275
276 8
        $this->_object->name = $base_name;
0 ignored issues
show
Bug Best Practice introduced by
The property name does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
277
        // decrementer, do not try more than this many times (the incrementer can raise above this if we start high enough.
278 8
        $d = 100;
279
280
        // The loop, usually we *should* hit gold in first try
281
        do {
282 8
            if ($i > 1) {
283
                // Start suffixes from -002
284 4
                $this->_object->{$name_prop} = $base_name . sprintf('-%03d', $i) . $extension;
285
            }
286
287
            // Handle the decrementer
288 8
            --$d;
289 8
            if ($d < 1) {
290
                // Decrementer underflowed
291
                debug_add("Maximum number of tries exceeded, current name was: " . $this->_object->{$name_prop}, MIDCOM_LOG_ERROR);
292
                $this->_object->{$name_prop} = $original_name;
293
                return false;
294
            }
295
            // and the incrementer
296 8
            ++$i;
297 8
        } while (!$this->name_is_unique());
298
299
        // Get a copy of the current, usable name
300 8
        $ret = (string)$this->_object->{$name_prop};
301
        // Restore the original name
302 8
        $this->_object->{$name_prop} = $original_name;
303 8
        return $ret;
304
    }
305
306 86
    private function _get_sibling_qb(string $schema_type, $parent)
307
    {
308 86
        $dummy = new $schema_type();
309 86
        $child_name_property = midcom_helper_reflector::get_name_property($dummy);
310 86
        if (empty($child_name_property)) {
311
            // This sibling class does not use names
312 86
            return false;
313
        }
314 86
        $resolver = midcom_helper_reflector_tree::get($schema_type);
315 86
        $qb = $resolver->_child_objects_type_qb($schema_type, $parent, false);
316 86
        if (!is_object($qb)) {
317
            return false;
318
        }
319
        // Do not include current object in results, this is the easiest way
320 86
        if (!empty($this->_object->guid)) {
321 17
            $qb->add_constraint('guid', '<>', $this->_object->guid);
322
        }
323 86
        $qb->add_order($child_name_property, 'DESC');
324
        // One result should be enough
325 86
        $qb->set_limit(1);
326 86
        return $qb;
327
    }
328
329 5
    private function _get_root_qb(string $schema_type)
330
    {
331 5
        $dummy = new $schema_type();
332 5
        $child_name_property = midcom_helper_reflector::get_name_property($dummy);
333 5
        if (empty($child_name_property)) {
334
            // This sibling class does not use names
335 5
            return false;
336
        }
337 5
        $qb = midcom_helper_reflector_tree::get($schema_type)->_root_objects_qb(false);
338 5
        if (!$qb) {
339
            return false;
340
        }
341
342
        // Do not include current object in results, this is the easiest way
343 5
        if (!empty($this->_object->guid)) {
344
            $qb->add_constraint('guid', '<>', $this->_object->guid);
345
        }
346 5
        $qb->add_order($child_name_property, 'DESC');
347
        // One result should be enough
348 5
        $qb->set_limit(1);
349 5
        return $qb;
350
    }
351
352 8
    private function _parse_filename(string $name, string $extension, $default = 0) : array
353
    {
354 8
        if (preg_match('/(.*?)-([0-9]{3,})' . $extension . '$/', $name, $name_matches)) {
355
            // Name already has i and base parts, split them.
356
            return [(int) $name_matches[2], (string) $name_matches[1]];
357
        }
358
        // Defaults
359 8
        return [$default, $name];
360
    }
361
362
    /**
363
     * Resolve the base value for the incrementing suffix and for the name.
364
     *
365
     * @see midcom_helper_reflector_nameresolver::generate_unique_name()
366
     * @param string $current_name the "current name" of the object (might not be the actual name value see the title logic in generate_unique_name())
367
     * @param string $extension The file extension, when working with attachments
368
     * @return array first key is the resolved $i second is the $base_name, which is $current_name without numeric suffix
369
     */
370 8
    private function _generate_unique_name_resolve_i(string $current_name, string $extension) : array
371
    {
372 8
        list($i, $base_name) = $this->_parse_filename($current_name, $extension, 1);
373
374
        // Look for siblings with similar names and see if they have higher i.
375 8
        midcom::get()->auth->request_sudo('midcom.helper.reflector');
376 8
        $parent = midcom_helper_reflector_tree::get_parent($this->_object);
0 ignored issues
show
Bug introduced by
$this->_object of type midcom_core_dbaobject is incompatible with the type midgard\portable\api\mgdobject expected by parameter $object of midcom_helper_reflector_tree::get_parent(). ( Ignorable by Annotation )

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

376
        $parent = midcom_helper_reflector_tree::get_parent(/** @scrutinizer ignore-type */ $this->_object);
Loading history...
377 8
        if (!empty($parent->guid)) {
378
            // We have parent, check siblings
379 6
            $parent_resolver = new midcom_helper_reflector_tree($parent);
380 6
            $sibling_classes = $parent_resolver->get_child_classes();
381 6
            if (!in_array('midgard_attachment', $sibling_classes)) {
382 6
                $sibling_classes[] = 'midgard_attachment';
383
            }
384 6
            foreach ($sibling_classes as $schema_type) {
385 6
                $i = $this->process_schema_type($this->_get_sibling_qb($schema_type, $parent), $i, $schema_type, $base_name, $extension);
386
            }
387
        } else {
388
            // No parent, we might be a root level class
389 2
            $is_root_class = false;
390 2
            $root_classes = midcom_helper_reflector_tree::get_root_classes();
391 2
            foreach ($root_classes as $schema_type) {
392 2
                if (midcom::get()->dbfactory->is_a($this->_object, $schema_type)) {
393 1
                    $is_root_class = true;
394 2
                    break;
395
                }
396
            }
397 2
            if (!$is_root_class) {
398
                // This should not happen, logging error and returning true (even though it's potentially dangerous)
399 1
                midcom::get()->auth->drop_sudo();
400 1
                debug_add("Object " . get_class($this->_object) . " #" . $this->_object->id . " has no valid parent but is not listed in the root classes, don't know what to do, letting higher level decide", MIDCOM_LOG_ERROR);
401 1
                return [$i, $base_name];
402
            }
403 1
            foreach ($root_classes as $schema_type) {
404 1
                $i = $this->process_schema_type($this->_get_root_qb($schema_type), $i, $schema_type, $base_name, $extension);
405
            }
406
        }
407 7
        midcom::get()->auth->drop_sudo();
408
409 7
        return [$i, $base_name];
410
    }
411
412 7
    private function process_schema_type($qb, $i, string $schema_type, string $base_name, string $extension) : int
413
    {
414 7
        if (!$qb) {
415 7
            return $i;
416
        }
417 7
        $child_name_property = midcom_helper_reflector::get_name_property(new $schema_type);
418
419 7
        $qb->add_constraint($child_name_property, 'LIKE', "{$base_name}-%" . $extension);
420 7
        $siblings = $qb->execute();
421 7
        if (!empty($siblings)) {
422
            $sibling = $siblings[0];
423
            $sibling_name = $sibling->{$child_name_property};
424
425
            $sibling_i = $this->_parse_filename($sibling_name, $extension)[0];
426
            if ($sibling_i >= $i) {
427
                $i = $sibling_i + 1;
428
            }
429
        }
430 7
        return $i;
431
    }
432
}
433