Passed
Push — master ( 31dc4c...6913a1 )
by Andreas
21:20
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 23
CRAP Score 9.423

Importance

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

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 142
    public function __construct($object)
24
    {
25 142
        $this->_object = $object;
26 142
    }
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 null on failure
33
     */
34 142
    public function get_object_name(string $name_property = null) : ?string
35
    {
36 142
        if ($name_property === null) {
37 142
            $name_property = midcom_helper_reflector::get_name_property($this->_object);
38
        }
39 142
        if (    empty($name_property)
40 142
            || !midcom_helper_reflector::get($this->_object)->property_exists($name_property)) {
41
            // Could not resolve valid property
42 1
            return null;
43
        }
44 142
        return $this->_object->{$name_property};
45
    }
46
47
    /**
48
     * Checks for "clean" URL name
49
     *
50
     * @see http://trac.midgard-project.org/ticket/809
51
     * @param string $name_property property to use as "name", if left to default (null), will be reflected
52
     */
53 1
    public function name_is_clean(string $name_property = null) : bool
54
    {
55 1
        if ($name_copy = $this->get_object_name($name_property)) {
56 1
            return $name_copy === midcom_helper_misc::urlize($name_copy);
57
        }
58
        // empty name is not "clean"
59
        return false;
60
    }
61
62
    /**
63
     * Checks for URL-safe name
64
     *
65
     * @see http://trac.midgard-project.org/ticket/809
66
     * @param string $name_property property to use as "name", if left to default (null), will be reflected
67
     */
68 90
    public function name_is_safe(string $name_property = null) : bool
69
    {
70 90
        if ($name_copy = $this->get_object_name($name_property)) {
71 90
            return $name_copy === rawurlencode($name_copy);
72
        }
73
        // empty name is not url-safe
74
        return false;
75
    }
76
77
    /**
78
     * Check that none of given object's siblings have same name.
79
     */
80 89
    public function name_is_unique() : bool
81
    {
82
        // Get current name and sanity-check
83 89
        $name = $this->get_object_name();
84 89
        if (empty($name)) {
85
            // We do not check for empty names, and do not consider them to be unique
86
            return false;
87
        }
88
89
        // Start the magic
90 89
        midcom::get()->auth->request_sudo('midcom.helper.reflector');
91 89
        $parent = $this->_object->get_parent();
92 89
        $stat = $this->check_sibling_classes($name, $this->get_sibling_classes($parent), $parent);
93 89
        midcom::get()->auth->drop_sudo();
94 89
        return $stat;
95
    }
96
97 89
    private function get_sibling_classes($parent = null) : array
98
    {
99 89
        if (!empty($parent->guid)) {
100
            // We have parent, check siblings
101 84
            $parent_resolver = new midcom_helper_reflector_tree($parent);
102 84
            $sibling_classes = $parent_resolver->get_child_classes();
103 84
            if (!in_array('midgard_attachment', $sibling_classes)) {
104 84
                $sibling_classes[] = 'midgard_attachment';
105
            }
106
107 84
            return $sibling_classes;
108
        }
109
        // No parent, we might be a root level class
110 9
        $root_classes = midcom_helper_reflector_tree::get_root_classes();
111 9
        foreach ($root_classes as $classname) {
112 9
            if (midcom::get()->dbfactory->is_a($this->_object, $classname)) {
113 5
                return $root_classes;
114
            }
115
        }
116 4
        debug_add("Object " . get_class($this->_object) . " #" . $this->_object->id . " has no valid parent but is not listed in the root classes", MIDCOM_LOG_ERROR);
117 4
        return [];
118
    }
119
120 89
    private function check_sibling_classes(string $name, array $schema_types, $parent = null) : bool
121
    {
122 89
        foreach ($schema_types as $schema_type) {
123 86
            $qb = $this->get_sibling_qb($schema_type, $parent);
124 86
            if (!$qb) {
125 86
                continue;
126
            }
127 86
            $child_name_property = midcom_helper_reflector::get_name_property(new $schema_type);
128
129 86
            $qb->add_constraint($child_name_property, '=', $name);
130 86
            if ($qb->count()) {
131 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

131
                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...
132 1
                return false;
133
            }
134
        }
135 89
        return true;
136
    }
137
138
    /**
139
     * Generates an unique name for the given object.
140
     *
141
     * 1st IF name is empty, we generate one from title (if title is empty too, we return null)
142
     * Then we check if it's unique, if not we add an incrementing
143
     * number to it (before this we make some educated guesses about a
144
     * good starting value)
145
     *
146
     * @param string $title_property Property of the object to use at title, if null will be reflected (see midcom_helper_reflector::get_object_title())
147
     * @param string $extension The file extension, when working with attachments
148
     */
149 71
    public function generate_unique_name(string $title_property = null, string $extension = '') : ?string
150
    {
151
        // Get current name and sanity-check
152 71
        $original_name = $this->get_object_name();
153 71
        if ($original_name === null) {
154
            // Fatal error with name resolution
155
            debug_add("Object " . get_class($this->_object) . " #{$this->_object->id} returned critical failure for name resolution, aborting", MIDCOM_LOG_WARN);
156
            return null;
157
        }
158
159
        // We need the name of the "name" property later
160 71
        $name_prop = midcom_helper_reflector::get_name_property($this->_object);
161
162 71
        if (!empty($original_name)) {
163
            $current_name = $original_name;
164
        } else {
165
            // Empty name, try to generate from title
166 71
            $title_copy = midcom_helper_reflector::get_object_title($this->_object, $title_property);
167 71
            if ($title_copy === null) {
168
                // Fatal error with title resolution
169
                debug_add("Object " . get_class($this->_object) . " #{$this->_object->id} returned critical failure for title resolution when name was empty, aborting", MIDCOM_LOG_WARN);
170
                return null;
171
            }
172 71
            if (empty($title_copy)) {
173 67
                debug_add("Object " . get_class($this->_object) . " #{$this->_object->id} has empty name and title, aborting", MIDCOM_LOG_WARN);
174 67
                return null;
175
            }
176 8
            $current_name = midcom_helper_misc::urlize($title_copy);
177 8
            unset($title_copy);
178
        }
179
180
        // incrementer, the number to add as suffix and the base name. see _generate_unique_name_resolve_i()
181 8
        [$i, $base_name] = $this->_generate_unique_name_resolve_i($current_name, $extension);
182
183 8
        $this->_object->{$name_prop} = $base_name;
184
        // decrementer, do not try more than this many times (the incrementer can raise above this if we start high enough.
185 8
        $d = 100;
186
187
        // The loop, usually we *should* hit gold in first try
188
        do {
189 8
            if ($i > 1) {
190
                // Start suffixes from -002
191
                $this->_object->{$name_prop} = $base_name . sprintf('-%03d', $i) . $extension;
192
            }
193
194
            // Handle the decrementer
195 8
            --$d;
196 8
            if ($d < 1) {
197
                // Decrementer underflowed
198
                debug_add("Maximum number of tries exceeded, current name was: " . $this->_object->{$name_prop}, MIDCOM_LOG_ERROR);
199
                $this->_object->{$name_prop} = $original_name;
200
                return null;
201
            }
202
            // and the incrementer
203 8
            ++$i;
204 8
        } while (!$this->name_is_unique());
205
206
        // Get a copy of the current, usable name
207 8
        $ret = (string)$this->_object->{$name_prop};
208
        // Restore the original name
209 8
        $this->_object->{$name_prop} = $original_name;
210 8
        return $ret;
211
    }
212
213 86
    private function get_sibling_qb(string $schema_type, $parent = null)
214
    {
215 86
        $child_name_property = midcom_helper_reflector::get_name_property(new $schema_type);
216 86
        if (empty($child_name_property)) {
217
            // This sibling class does not use names
218 86
            return false;
219
        }
220 86
        if ($parent === null) {
221 5
            $qb = midcom_helper_reflector_tree::get($schema_type)->_root_objects_qb();
222
        } else {
223 84
            $resolver = midcom_helper_reflector_tree::get($schema_type);
224 84
            $qb = $resolver->_child_objects_type_qb($schema_type, $parent, false);
225
        }
226 86
        if (!$qb) {
227
            return false;
228
        }
229
230
        // Do not include current object in results, this is the easiest way
231 86
        if (!empty($this->_object->guid)) {
232 15
            $qb->add_constraint('guid', '<>', $this->_object->guid);
233
        }
234 86
        $qb->add_order($child_name_property, 'DESC');
235
        // One result should be enough
236 86
        $qb->set_limit(1);
237 86
        return $qb;
238
239
    }
240
241 8
    private function _parse_filename(string $name, string $extension, int $default = 0) : array
242
    {
243 8
        if (preg_match('/(.*?)-([0-9]{3,})' . $extension . '$/', $name, $name_matches)) {
244
            // Name already has i and base parts, split them.
245
            return [(int) $name_matches[2], (string) $name_matches[1]];
246
        }
247
        // Defaults
248 8
        return [$default, $name];
249
    }
250
251
    /**
252
     * Resolve the base value for the incrementing suffix and for the name.
253
     *
254
     * @see midcom_helper_reflector_nameresolver::generate_unique_name()
255
     * @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())
256
     * @param string $extension The file extension, when working with attachments
257
     * @return array first key is the resolved $i second is the $base_name, which is $current_name without numeric suffix
258
     */
259 8
    private function _generate_unique_name_resolve_i(string $current_name, string $extension) : array
260
    {
261 8
        [$i, $base_name] = $this->_parse_filename($current_name, $extension, 1);
262
263
        // Look for siblings with similar names and see if they have higher i.
264 8
        midcom::get()->auth->request_sudo('midcom.helper.reflector');
265 8
        $parent = $this->_object->get_parent();
266 8
        foreach ($this->get_sibling_classes($parent) as $schema_type) {
267 7
            if ($qb = $this->get_sibling_qb($schema_type, $parent)) {
268 7
                $child_name_property = midcom_helper_reflector::get_name_property(new $schema_type);
269
270 7
                $qb->add_constraint($child_name_property, 'LIKE', "{$base_name}-%" . $extension);
271 7
                if ($siblings = $qb->execute()) {
272
                    $sibling = $siblings[0];
273
                    $sibling_name = $sibling->{$child_name_property};
274
275
                    $sibling_i = $this->_parse_filename($sibling_name, $extension)[0];
276
                    if ($sibling_i >= $i) {
277
                        $i = $sibling_i + 1;
278
                    }
279
                }
280
            }
281
        }
282 8
        midcom::get()->auth->drop_sudo();
283
284 8
        return [$i, $base_name];
285
    }
286
}
287