Passed
Push — dev ( 424ba7...b88f8d )
by Marcin
07:00
created

Converter   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 279
Duplicated Lines 0 %

Test Coverage

Coverage 83.84%

Importance

Changes 12
Bugs 0 Features 0
Metric Value
eloc 109
c 12
b 0
f 0
dl 0
loc 279
ccs 83
cts 99
cp 0.8384
rs 8.64
wmc 47

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
B getClassesMapping() 0 27 7
B getClassMappingConfigOrThrow() 0 32 7
B getPrimitivesMapping() 0 27 7
B convertArray() 0 33 8
B convert() 0 38 11
A hasNonNumericKeys() 0 9 3
A getPrimitiveMappingConfigOrThrow() 0 15 3

How to fix   Complexity   

Complex Class

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

1
<?php
2
declare(strict_types=1);
3
4
namespace MarcinOrlowski\ResponseBuilder;
5
6
/**
7
 * Laravel API Response Builder
8
 *
9
 * @package   MarcinOrlowski\ResponseBuilder
10
 *
11
 * @author    Marcin Orlowski <mail (#) marcinOrlowski (.) com>
12
 * @copyright 2016-2020 Marcin Orlowski
13
 * @license   http://www.opensource.org/licenses/mit-license.php MIT
14
 * @link      https://github.com/MarcinOrlowski/laravel-api-response-builder
15
 */
16
17
use Illuminate\Support\Facades\Log;
18
use Illuminate\Support\Facades\Config;
19
20
21
/**
22
 * Data converter
23
 */
24
class Converter
25
{
26
    /** @var array */
27
    protected $classes = [];
28
29
    /** @var array */
30
    protected $primitives = [];
31
32
    /** @var bool */
33
    protected $debug_enabled = false;
34
35
    /**
36
     * Converter constructor.
37
     *
38
     * @throws \RuntimeException
39
     */
40 43
    public function __construct()
41
    {
42 43
        $this->classes = static::getClassesMapping() ?? [];
43 42
        $this->primitives = static::getPrimitivesMapping() ?? [];
44
45 40
	    $this->debug_enabled = Config::get(ResponseBuilder::CONF_KEY_CONVERTER_DEBUG_KEY, false);
46 40
    }
47
48
	/**
49
	 * Returns "converter/primitives" entry for given primitive object or throws exception if no config found.
50
	 * Throws \RuntimeException if there's no config "classes" mapping entry for this object configured.
51
	 * Throws \InvalidArgumentException if No data conversion mapping configured for given class.
52
	 *
53 5
	 * @param boolean|string|double|array|int $data Primitive to get config for.
54
	 *
55 5
	 * @return array
56
	 *
57
	 * @throws \InvalidArgumentException
58
	 */
59
    protected function getPrimitiveMappingConfigOrThrow($data): array
60
    {
61
	    $result = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
62
63
	    $type = \gettype($data);
64
	    $result = $this->primitives[ $type ] ?? null;
65
	    if ($result === null) {
66
		    throw new \InvalidArgumentException(sprintf('No data conversion mapping configured for "%s" primitive.', $type));
67
	    }
68
69
	    if ($this->debug_enabled) {
70
		    Log::debug(__CLASS__ . ": Converting primitive type of '{$type}' to data node '{$result[ResponseBuilder::KEY_KEY]}'.");
71
	    }
72
73
	    return $result;
74
    }
75
76
    /**
77
     * Returns "converter/map" mapping configured for given $data object class or throws exception if not found.
78
     * Throws \RuntimeException if there's no config "classes" mapping entry for this object configured.
79 14
     * Throws \InvalidArgumentException if No data conversion mapping configured for given class.
80
     *
81 14
     * @param object $data Object to get config for.
82
     *
83 14
     * @return array
84 14
     *
85 14
     * @throws \InvalidArgumentException
86
     */
87
    protected function getClassMappingConfigOrThrow(object $data): array
88
    {
89 14
        $result = null;
90
        $debug_result = '';
91
92
        // check for exact class name match...
93 14
        $cls = \get_class($data);
94
        if (\is_string($cls)) {
0 ignored issues
show
introduced by
The condition is_string($cls) is always true.
Loading history...
95
	        if (\array_key_exists($cls, $this->classes)) {
96
		        $result = $this->classes[ $cls ];
97
		        $debug_result = 'exact config match';
98
	        } else {
99
		        // no exact match, then lets try with `instanceof`
100
		        foreach (\array_keys($this->classes) as $class_name) {
101
			        if ($data instanceof $class_name) {
102
				        $result = $this->classes[ $class_name ];
103
				        $debug_result = "subclass of {$class_name}";
104
				        break;
105
			        }
106
		        }
107 16
	        }
108
        }
109 16
110 16
        if ($result === null) {
111
            throw new \InvalidArgumentException(sprintf('No data conversion mapping configured for "%s" class.', $cls));
112
        }
113 16
114 16
        if ($this->debug_enabled) {
115 16
			Log::debug(__CLASS__ . ": Converting {$cls} using {$result[ResponseBuilder::KEY_HANDLER]} because: {$debug_result}.");
116 11
        }
117 11
118
	    return $result;
119
    }
120 5
121 5
    /**
122 4
     * Main entry for data conversion
123 4
     *
124 4
     * @param object|array|null $data
125
     *
126
     * @return mixed|null
127
     *
128
     * @throws \InvalidArgumentException
129
     */
130 16
    public function convert($data = null): ?array
131 1
    {
132
        if ($data === null) {
133
            return null;
134 15
        }
135
136
        $result = null;
137
138 15
	    Validator::assertIsType('data', $data, [
139
		    Validator::TYPE_ARRAY,
140
		    Validator::TYPE_BOOL,
141
		    Validator::TYPE_DOUBLE,
142
		    Validator::TYPE_INTEGER,
143
		    Validator::TYPE_STRING,
144
		    Validator::TYPE_OBJECT,
145
	    ]);
146
147
	    if ($result === null && \is_object($data)) {
148
		    $cfg = $this->getClassMappingConfigOrThrow($data);
149
		    $worker = new $cfg[ ResponseBuilder::KEY_HANDLER ]();
150
		    $result = [$cfg[ ResponseBuilder::KEY_KEY ] => $worker->convert($data, $cfg)];
151
	    }
152
153
	    if ($result === null && \is_array($data)) {
154
	        $cfg = $this->getPrimitiveMappingConfigOrThrow($data);
155
156
	        if ($this->hasNonNumericKeys($data)){
157
		        $result = $this->convertArray($data);
158
	        } else {
159
		        $result = [$cfg[ ResponseBuilder::KEY_KEY ] => $this->convertArray($data)];
160
	        }
161
        }
162
163
	    if ( \is_bool($data) || \is_float($data) || \is_int($data) || \is_string($data)) {
0 ignored issues
show
introduced by
The condition is_int($data) is always false.
Loading history...
introduced by
The condition is_string($data) is always false.
Loading history...
introduced by
The condition is_float($data) is always false.
Loading history...
164
		    $result = [$this->getPrimitiveMappingConfigOrThrow($data)[ ResponseBuilder::KEY_KEY ] => $data];
165
	    }
166
167
	    return $result;
168
    }
169
170
	/**
171
	 * Checks if given array uses custom (non numeric) keys.
172
	 *
173
	 * @param array $data
174
	 *
175
	 * @return bool
176
	 */
177
    protected function hasNonNumericKeys(array $data): bool
178
    {
179
	    foreach (\array_keys($data) as $key) {
180
	    	if (!\is_int($key)) {
181
	    		return true;
182
		    }
183
    	}
184 40
185
	    return false;
186 40
    }
187 15
188
    /**
189
     * Recursively walks $data array and converts all known objects if found. Note
190 25
     * $data array is passed by reference so source $data array may be modified.
191
     *
192 25
     * @param array $data array to recursively convert known elements of
193 25
     *
194 25
     * @return array
195 25
     *
196 25
     * @throws \RuntimeException
197 25
     */
198 25
    protected function convertArray(array $data): array
199
    {
200
        // This is to ensure that we either have array with user provided keys i.e. ['foo'=>'bar'], which will then
201 25
        // be turned into JSON object or array without user specified keys (['bar']) which we would return as JSON
202 11
        // array. But you can't mix these two as the final JSON would not produce predictable results.
203 10
        $string_keys_cnt = 0;
204 10
        $int_keys_cnt = 0;
205
        foreach ($data as $key => $val) {
206
            if (\is_int($key)) {
207 24
                $int_keys_cnt++;
208 10
            } else {
209
                $string_keys_cnt++;
210 10
            }
211 6
212
            if (($string_keys_cnt > 0) && ($int_keys_cnt > 0)) {
213 4
                throw new \RuntimeException(
214
                    'Invalid data array. Either set own keys for all the items or do not specify any keys at all. ' .
215
                    'Arrays with mixed keys are not supported by design.');
216
            }
217 23
        }
218 4
219
        foreach ($data as $key => $val) {
220
            if (\is_array($val)) {
221 23
                $data[ $key ] = $this->convertArray($val);
222
            } elseif (\is_object($val)) {
223
                $cfg = $this->getClassMappingConfigOrThrow($val);
224
                $worker = new $cfg[ ResponseBuilder::KEY_HANDLER ]();
225
                $converted_data = $worker->convert($val, $cfg);
226
                $data[ $key ] = $converted_data;
227
            }
228
        }
229
230
        return $data;
231 10
    }
232
233 10
    /**
234 10
     * Reads and validates "converter/map" config mapping
235 6
     *
236
     * @return array Classes mapping as specified in configuration or empty array if configuration found
237
     *
238
     * @throws \RuntimeException if config mapping is technically invalid (i.e. not array etc).
239 4
     */
240
    protected static function getClassesMapping(): array
241
    {
242
        $classes = Config::get(ResponseBuilder::CONF_KEY_CONVERTER_CLASSES) ?? [];
243
244
	    if (!\is_array($classes)) {
245
		    throw new \RuntimeException(
246
			    \sprintf('CONFIG: "%s" mapping must be an array (%s given)', ResponseBuilder::CONF_KEY_CONVERTER_CLASSES, \gettype($classes)));
247
	    }
248
249
	    if (!empty($classes)) {
250
		    $mandatory_keys = [
251
			    ResponseBuilder::KEY_HANDLER,
252 10
			    ResponseBuilder::KEY_KEY,
253
		    ];
254
		    foreach ($classes as $class_name => $class_config) {
255
			    if (!\is_array($class_config)) {
256
				    throw new \InvalidArgumentException(sprintf("CONFIG: Config for '{$class_name}' class must be an array (%s given).", \gettype($class_config)));
257 10
			    }
258 10
			    foreach ($mandatory_keys as $key_name) {
259 10
				    if (!\array_key_exists($key_name, $class_config)) {
260 10
					    throw new \RuntimeException("CONFIG: Missing '{$key_name}' for '{$class_name}' class mapping");
261 6
				    }
262
			    }
263 6
		    }
264
	    }
265
266 10
        return $classes;
267 1
    }
268
269 1
	/**
270
	 * Reads and validates "converter/primitives" config mapping
271
	 *
272
	 * @return array Primitives mapping config as specified in configuration or empty array if configuration found
273 9
	 *
274 9
	 * @throws \RuntimeException if config mapping is technically invalid (i.e. not array etc).
275 4
	 */
276 9
	protected static function getPrimitivesMapping(): array
277 5
	{
278 5
		$primitives = Config::get(ResponseBuilder::CONF_KEY_CONVERTER_PRIMITIVES) ?? [];
279 5
280 5
		if (!\is_array($primitives)) {
281
			throw new \RuntimeException(
282
				\sprintf('CONFIG: "%s" mapping must be an array (%s given)', ResponseBuilder::CONF_KEY_CONVERTER_PRIMITIVES, \gettype($primitives)));
283
		}
284 9
285
		if (!empty($primitives)) {
286
			$mandatory_keys = [
287
				ResponseBuilder::KEY_KEY,
288
			];
289
290
			foreach ($primitives as $type => $config) {
291
				if (!\is_array($config)) {
292
					throw new \InvalidArgumentException(sprintf("CONFIG: Config for '{$type}' primitive must be an array (%s given).", \gettype($config)));
293
				}
294 47
				foreach ($mandatory_keys as $key_name) {
295
					if (!\array_key_exists($key_name, $config)) {
296 47
						throw new \RuntimeException("CONFIG: Missing '{$key_name}' for '{$type}' primitive mapping");
297
					}
298 47
				}
299 3
			}
300 3
		}
301
302
		return $primitives;
303 44
	}
304
}
305