AbstractArgumentBuilder   F
last analyzed

Complexity

Total Complexity 60

Size/Duplication

Total Lines 355
Duplicated Lines 0 %

Test Coverage

Coverage 98.41%

Importance

Changes 0
Metric Value
wmc 60
eloc 124
dl 0
loc 355
ccs 124
cts 126
cp 0.9841
rs 3.6
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A camelCaseToSnakeCase() 0 7 1
B callSet() 0 40 11
A load() 0 2 1
A __toString() 0 3 1
B callUnset() 0 45 9
A build() 0 13 3
B normalizeFields() 0 37 8
B callGet() 0 30 10
A __call() 0 11 3
A snakeCaseToCamelCase() 0 3 1
A transformValue() 0 11 4
B validateFieldValue() 0 27 7
A __construct() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like AbstractArgumentBuilder 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 AbstractArgumentBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Feedo\ArgumentBuilder;
6
7
use Feedo\ArgumentBuilder\Validator\ArgumentBuilderTypeValidator;
8
use Feedo\ArgumentBuilder\Validator\TypeValidatorInterface;
9
10
/**
11
 * Class AbstractArgumentBuilder.
12
 *
13
 * @author Denis Voytyuk <[email protected]>
14
 */
15
abstract class AbstractArgumentBuilder implements ArgumentBuilderInterface
16
{
17
    public const ARGUMENT_TYPE_MIXED = 0;
18
    public const ARGUMENT_TYPE_ARGUMENT_BUILDER = 1;
19
    public const ARGUMENT_TYPE_NUMERIC = 2;
20
    public const ARGUMENT_TYPE_ENUM = 3;
21
    public const ARGUMENT_TYPE_BOOLEAN = 4;
22
23
    protected $args = array();
24
25
    protected $fields = array(
26
        //'arg1' => self::ARGUMENT_TYPE_MIXED,
27
        //'arg2' => SomeArgumentBuilder::class,
28
    );
29
30 38
    public function __construct()
31
    {
32 38
        $this->load();
33 38
        $this->normalizeFields();
34 35
    }
35
36 38
    protected function load()
37
    {
38 38
    }
39
40
    /**
41
     * Translates a camel case string into a string with underscores (e.g. firstName -&gt; first_name).
42
     *
43
     * @param string $str String in camel case format
44
     *
45
     * @return string $str Translated into underscore format
46
     */
47 35
    private function camelCaseToSnakeCase($str)
48
    {
49 35
        $str[0] = strtolower($str[0]);
50
51
        return preg_replace_callback('/([A-Z])/', function ($c) {
52
            return '_'.strtolower($c[1]);
53 35
        }, $str);
54
    }
55
56
    /**
57
     * Translates a snake case string into a camel-cased string (e.g. first_name -&gt; FirstName).
58
     *
59
     * @param string $str
60
     *
61
     * @return string
62
     */
63 27
    private function snakeCaseToCamelCase($str)
64
    {
65 27
        return implode(array_map('ucfirst', explode('_', $str)));
66
    }
67
68
    /**
69
     * Normalizes field definitions so that they have the same format (expands shortcuts to array).
70
     *
71
     * @throws Exception\InvalidDefinitionException
72
     */
73 38
    private function normalizeFields()
74
    {
75 38
        foreach ($this->fields as $name => $field) {
76
            // Consider string value as a class name
77 38
            if (is_string($field)) {
78 28
                $this->fields[$name] = array(
79 28
                    'type' => self::ARGUMENT_TYPE_ARGUMENT_BUILDER,
80 28
                    'class' => $field,
81 28
                    'validator' => new ArgumentBuilderTypeValidator($name, $field),
82
                );
83
84
            // consider numeric values as type name
85 37
            } elseif (is_int($field)) {
86 27
                $this->fields[$name] = array(
87 27
                    'type' => $field,
88
                    'validator' => null,
89
                );
90
            }
91
92 38
            if (!is_array($this->fields[$name])) {
93 1
                throw new Exception\InvalidDefinitionException(
94 1
                    'Field description must be either string (shortcut for class name), or int (shortcut for field type) or array (full form)'
95
                );
96
            }
97
98 37
            if (!array_key_exists('type', $this->fields[$name])) {
99 1
                throw new Exception\InvalidDefinitionException(
100 1
                    'Field type is not defined'
101
                );
102
            }
103
104
            if (
105 36
                self::ARGUMENT_TYPE_ARGUMENT_BUILDER === $this->fields[$name]['type']
106 36
                && empty($this->fields[$name]['class'])
107
            ) {
108 1
                throw new Exception\InvalidDefinitionException(
109 36
                    'Field of type ARGUMENT_TYPE_ARGUMENT_BUILDER must have class defined'
110
                );
111
            }
112
        }
113 35
    }
114
115
    /**
116
     * Validates non-null fields according to the field type and validator (if given).
117
     *
118
     * @param string $field
119
     * @param mixed  $value
120
     *
121
     * @return bool
122
     *
123
     * @throws Exception\InvalidDefinitionException
124
     */
125 35
    private function validateFieldValue($field, $value)
126
    {
127 35
        if (null === $value) {
128 1
            return true;
129
        }
130
131
        if (
132 35
            !array_key_exists('validator', $this->fields[$field])
133 35
            || null === $this->fields[$field]['validator']
134 35
            || false === $this->fields[$field]['validator']
135
        ) {
136 27
            return true;
137
        }
138
139 35
        $validator = $this->fields[$field]['validator'];
140
141 35
        if ($validator instanceof TypeValidatorInterface) {
142 3
            return $validator->validate($value);
143
        }
144
145 34
        if (!is_callable($validator)) {
146 6
            throw new Exception\InvalidDefinitionException(
147 6
                sprintf('Validator for the field "%s" is defined but is not callable', $field)
148
            );
149
        }
150
151 28
        return call_user_func($this->fields[$field]['validator'], $value);
152
    }
153
154
    /**
155
     * @param string $name
156
     * @param array  $arguments
157
     *
158
     * @return mixed
159
     *
160
     * @throws Exception\UndefinedMethodException
161
     */
162 35
    public function __call($name, $arguments)
163
    {
164 35
        foreach (array('callGet', 'callSet', 'callUnset') as $method) {
165
            try {
166 35
                return $this->{$method}($name, $arguments);
167 35
            } catch (Exception\UnmatchedCallTypeException $e) {
168
                // continue cycle
169
            }
170
        }
171
172 1
        throw new Exception\UndefinedMethodException($name);
173
    }
174
175
    /**
176
     * Provides $builder->getName($arguments...).
177
     *
178
     * @param string $name
179
     * @param array  $arguments
180
     *
181
     * @return mixed|null
182
     *
183
     * @throws Exception\UnmatchedCallTypeException
184
     * @throws Exception\InvalidArgumentException
185
     * @throws Exception\UndefinedMethodException
186
     */
187 35
    private function callGet($name, $arguments)
188
    {
189 35
        if (!preg_match('/^get([A-Z\-][A-Za-z0-9]+)$/', $name, $matches)) {
190 35
            throw new Exception\UnmatchedCallTypeException();
191
        }
192
193 7
        $field = lcfirst($matches[1]);
194 7
        $field = $this->camelCaseToSnakeCase($field);
195
196 7
        if (!array_key_exists($field, $this->fields)) {
197 1
            throw new Exception\UndefinedMethodException($name);
198
        }
199
200 6
        if (self::ARGUMENT_TYPE_ARGUMENT_BUILDER !== $this->fields[$field]['type'] && 0 !== count($arguments)) {
201 1
            throw new Exception\InvalidArgumentException('Method '.__CLASS__.'::'.$name.'() must take exactly 0 arguments');
202
        }
203
204 5
        if (self::ARGUMENT_TYPE_ARGUMENT_BUILDER === $this->fields[$field]['type'] && count($arguments) > 0) {
205 5
            if (!is_string($arguments[0])) {
206 4
                throw new Exception\InvalidArgumentException(
207 4
                    'Method '.__CLASS__.'::'.$name.'() expects the first parameter to be string if you want to get sub-value'
208
                );
209
            }
210
211 1
            if (!empty($this->args[$field])) {
212 1
                return call_user_func_array(array($this->args[$field], 'get'.$this->snakeCaseToCamelCase($arguments[0])), array_slice($arguments, 1));
213
            }
214
        }
215
216 1
        return isset($this->args[$field]) ? $this->args[$field] : null;
217
    }
218
219
    /**
220
     * Provides $builder->setName($value).
221
     *
222
     * @param string $name
223
     * @param array  $arguments
224
     *
225
     * @return $this
226
     *
227
     * @throws Exception\InvalidDefinitionException
228
     * @throws Exception\UndefinedMethodException
229
     * @throws Exception\UnmatchedCallTypeException
230
     */
231 35
    private function callSet($name, $arguments)
232
    {
233 35
        if (!preg_match('/^set([A-Z\-][A-Za-z0-9]+)$/', $name, $matches)) {
234 9
            throw new Exception\UnmatchedCallTypeException();
235
        }
236
237 35
        $field = $matches[1];
238 35
        if ('-' !== $field[0]) {
239 35
            $field = lcfirst($matches[1]);
240
        }
241 35
        $field = $this->camelCaseToSnakeCase($field);
242
243 35
        if (!array_key_exists($field, $this->fields)) {
244 1
            throw new Exception\UndefinedMethodException($name);
245
        }
246
247 35
        if (self::ARGUMENT_TYPE_ARGUMENT_BUILDER !== $this->fields[$field]['type'] && 1 !== count($arguments)) {
248 1
            throw new Exception\InvalidArgumentException('Method '.__CLASS__.'::'.$name.'() must take exactly 1 argument');
249
        }
250
251 35
        $value = $arguments[0];
252
253 35
        if (self::ARGUMENT_TYPE_ARGUMENT_BUILDER === $this->fields[$field]['type'] && count($arguments) > 1) {
254 27
            if (!is_string($arguments[0])) {
255 4
                throw new Exception\InvalidArgumentException(
256 4
                    'Method '.__CLASS__.'::'.$name.'() expects the first parameter to be string'
257
                );
258
            }
259
260 27
            $class = $this->fields[$field]['class'];
261 27
            $value = isset($this->args[$field]) ? $this->args[$field] : new $class();
262
263 27
            call_user_func_array(array($value, 'set'.$this->snakeCaseToCamelCase($arguments[0])), array_slice($arguments, 1));
264 35
        } elseif (!$this->validateFieldValue($field, $value)) {
265 1
            throw new Exception\InvalidArgumentException(sprintf('Invalid value "%s" for field "%s"', $value, $field));
266
        }
267
268 27
        $this->args[$field] = $value;
269
270 27
        return $this;
271
    }
272
273
    /**
274
     * Provides $builder->unsetName().
275
     *
276
     * @param string $name
277
     * @param array  $arguments
278
     *
279
     * @return $this
280
     *
281
     * @throws Exception\UnmatchedCallTypeException
282
     * @throws Exception\UndefinedMethodException
283
     */
284 9
    private function callUnset($name, $arguments)
285
    {
286 9
        if (!preg_match('/^unset([A-Z\-][A-Za-z0-9]+)$/', $name, $matches)) {
287 1
            throw new Exception\UnmatchedCallTypeException();
288
        }
289
290 8
        $field = $matches[1];
291 8
        if ('-' !== $field[0]) {
292 8
            $field = lcfirst($matches[1]);
293
        }
294 8
        $field = $this->camelCaseToSnakeCase($field);
295
296
        // Check if field is defined for this ArgumentBuilder
297 8
        if (!array_key_exists($field, $this->fields)) {
298 1
            throw new Exception\UndefinedMethodException($name);
299
        }
300
301
        // Check argument count
302 7
        if (self::ARGUMENT_TYPE_ARGUMENT_BUILDER !== $this->fields[$field]['type'] && 0 !== count($arguments)) {
303 1
            throw new Exception\InvalidArgumentException('Method '.__CLASS__.'::'.$name.'() must take exactly 0 arguments');
304
        }
305
306
        // no parameters means plain unsetName() call, so no syntatic sugar logic involved
307 6
        if (0 === count($arguments)) {
308 2
            unset($this->args[$field]);
309
310 2
            return $this;
311
        }
312
313
        // At this point we already filtered out zero arguments and non-ARGUMENT_BUILDER types,
314
        // so we expect that the first argument is string because it's a name for a field.
315
        // Let's check that!
316 5
        if (!is_string($arguments[0])) {
317 4
            throw new Exception\InvalidArgumentException(
318 4
                'Method '.__CLASS__.'::'.$name.'() expects the first parameter to be string if you want to unset sub-value'
319
            );
320
        }
321
322
        // To call unset on a sub-ArgumentBuilder, we need to make sure it exists (otherwise there is nothing to unset)
323 1
        if (!empty($this->args[$field])) {
324
            // Allow syntactic sugar (pass unset call recursively)
325 1
            call_user_func_array(array($this->args[$field], 'unset'.$this->snakeCaseToCamelCase($arguments[0])), array_slice($arguments, 1));
326
        }
327
328 1
        return $this;
329
    }
330
331 5
    private function transformValue($field, $value)
332
    {
333 5
        if (self::ARGUMENT_TYPE_BOOLEAN === $this->fields[$field]['type']) {
334 5
            if ('any' === $value) {
335
                return $value;
336
            }
337
338 5
            return $value ? 'true' : 'false';
339
        }
340
341 5
        return $value;
342
    }
343
344
    /**
345
     * Returns array of arguments.
346
     *
347
     * @return array
348
     */
349 5
    public function build()
350
    {
351 5
        $result = array();
352
353 5
        foreach ($this->args as $key => $arg) {
354 5
            if ($arg instanceof ArgumentBuilderInterface) {
355 5
                $result[$key] = $arg->build(); //@todo: missing Circular Reference check
356
            } else {
357 5
                $result[$key] = $this->transformValue($key, $arg);
358
            }
359
        }
360
361 5
        return $result;
362
    }
363
364
    /**
365
     * @return string
366
     */
367 1
    public function __toString()
368
    {
369 1
        return http_build_query($this->build());
370
    }
371
}
372