Passed
Push — master ( 0d48f6...0a3404 )
by Andreas
24:10
created

midcom_helper_reflector_nameresolver   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 299
Duplicated Lines 0 %

Test Coverage

Coverage 87.5%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 128
c 2
b 0
f 0
dl 0
loc 299
ccs 119
cts 136
cp 0.875
rs 8.8798
wmc 44

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A name_is_safe() 0 7 2
A get_object_name() 0 11 4
A name_is_clean() 0 7 2
A _generate_unique_name_resolve_i() 0 20 3
A get_sibling_qb() 0 26 5
A _parse_filename() 0 8 2
A check_sibling_classes() 0 16 4
A name_is_unique() 0 22 3
B generate_unique_name() 0 62 8
A process_schema_type() 0 19 4
A get_sibling_classes() 0 25 6

How to fix   Complexity   

Complex Class

Complex classes like midcom_helper_reflector_nameresolver often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use midcom_helper_reflector_nameresolver, and based on these observations, apply Extract Interface, too.

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 140
    public function __construct($object)
24
    {
25 140
        $this->_object = $object;
26 140
    }
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 140
    public function get_object_name(string $name_property = null) : ?string
35
    {
36 140
        if ($name_property === null) {
37 140
            $name_property = midcom_helper_reflector::get_name_property($this->_object);
38
        }
39 140
        if (    empty($name_property)
40 140
            || !midcom_helper_reflector::get($this->_object)->property_exists($name_property)) {
41
            // Could not resolve valid property
42 1
            return null;
43
        }
44 140
        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 4
            return false;
87
        }
88
89
        // Start the magic
90 89
        midcom::get()->auth->request_sudo('midcom.helper.reflector');
91 89
        $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

91
        $parent = midcom_helper_reflector_tree::get_parent(/** @scrutinizer ignore-type */ $this->_object);
Loading history...
92 89
        $sibling_classes = $this->get_sibling_classes($parent);
93 89
        if ($sibling_classes === null) {
94
            // This should not happen, logging error and returning true (even though it's potentially dangerous)
95 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);
96 4
            return true;
97
        }
98 86
        $stat = $this->check_sibling_classes($name, $sibling_classes, $parent);
99
100 86
        midcom::get()->auth->drop_sudo();
101 86
        return $stat;
102
    }
103
104 89
    private function get_sibling_classes($parent = null) : ?array
105
    {
106 89
        if (!empty($parent->guid)) {
107
            // We have parent, check siblings
108 84
            $parent_resolver = new midcom_helper_reflector_tree($parent);
109 84
            $sibling_classes = $parent_resolver->get_child_classes();
110 84
            if (!in_array('midgard_attachment', $sibling_classes)) {
111 84
                $sibling_classes[] = 'midgard_attachment';
112
            }
113
114 84
            return $sibling_classes;
115
        }
116
        // No parent, we might be a root level class
117 10
        $is_root_class = false;
118 10
        $root_classes = midcom_helper_reflector_tree::get_root_classes();
119 10
        foreach ($root_classes as $classname) {
120 10
            if (midcom::get()->dbfactory->is_a($this->_object, $classname)) {
121 6
                $is_root_class = true;
122 6
                break;
123
            }
124
        }
125 10
        if (!$is_root_class) {
126 4
            return null;
127
        }
128 6
        return $root_classes;
129
    }
130
131 86
    private function check_sibling_classes(string $name, array $schema_types, $parent = null) : bool
132
    {
133 86
        foreach ($schema_types as $schema_type) {
134 86
            $qb = $this->get_sibling_qb($schema_type, $parent);
135 86
            if (!$qb) {
136 86
                continue;
137
            }
138 86
            $child_name_property = midcom_helper_reflector::get_name_property(new $schema_type);
139
140 86
            $qb->add_constraint($child_name_property, '=', $name);
141 86
            if ($qb->count()) {
142 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

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

278
        $parent = midcom_helper_reflector_tree::get_parent(/** @scrutinizer ignore-type */ $this->_object);
Loading history...
279 8
        $sibling_classes = $this->get_sibling_classes($parent);
280 8
        if ($sibling_classes === null) {
281
            // This should not happen, logging error and returning true (even though it's potentially dangerous)
282 1
            midcom::get()->auth->drop_sudo();
283 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);
284 1
            return [$i, $base_name];
285
        }
286 7
        foreach ($sibling_classes as $schema_type) {
287 7
            $i = $this->process_schema_type($this->get_sibling_qb($schema_type, $parent), $i, $schema_type, $base_name, $extension);
288
        }
289 7
        midcom::get()->auth->drop_sudo();
290
291 7
        return [$i, $base_name];
292
    }
293
294 7
    private function process_schema_type($qb, $i, string $schema_type, string $base_name, string $extension) : int
295
    {
296 7
        if (!$qb) {
297 7
            return $i;
298
        }
299 7
        $child_name_property = midcom_helper_reflector::get_name_property(new $schema_type);
300
301 7
        $qb->add_constraint($child_name_property, 'LIKE', "{$base_name}-%" . $extension);
302 7
        $siblings = $qb->execute();
303 7
        if (!empty($siblings)) {
304
            $sibling = $siblings[0];
305
            $sibling_name = $sibling->{$child_name_property};
306
307
            $sibling_i = $this->_parse_filename($sibling_name, $extension)[0];
308
            if ($sibling_i >= $i) {
309
                $i = $sibling_i + 1;
310
            }
311
        }
312 7
        return $i;
313
    }
314
}
315