Passed
Push — dev ( da34c3...980f00 )
by Marcin
06:17
created

Converter::convert()   B

Complexity

Conditions 11
Paths 13

Size

Total Lines 37
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 51.1484

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 11
eloc 22
c 4
b 0
f 0
nc 13
nop 1
dl 0
loc 37
ccs 4
cts 13
cp 0.3076
crap 51.1484
rs 7.3166

How to fix   Complexity   

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
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
use MarcinOrlowski\ResponseBuilder\ResponseBuilder as RB;
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(RB::CONF_KEY_DEBUG_CONVERTER_DEBUG_ENABLED, 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[RB::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[RB::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
		    Type::ARRAY,
140
		    Type::BOOLEAN,
141
		    Type::DOUBLE,
142
		    Type::INTEGER,
143
		    Type::STRING,
144
		    Type::OBJECT,
145
	    ]);
146
147
	    if ($result === null && \is_object($data)) {
148
		    $cfg = $this->getClassMappingConfigOrThrow($data);
149
		    $worker = new $cfg[ RB::KEY_HANDLER ]();
150
		    $result = [$cfg[ RB::KEY_KEY ] => $worker->convert($data, $cfg)];
151
	    }
152
153
	    if ($result === null && \is_array($data)) {
154
	        $cfg = $this->getPrimitiveMappingConfigOrThrow($data);
155
156
		    $result = $this->convertArray($data);
157
	        if (!Util::isArrayWithNonNumericKeys($data)){
158
		        $result = [$cfg[ RB::KEY_KEY ] => $result];
159
	        }
160
        }
161
162
	    if ( \is_bool($data) || \is_float($data) || \is_int($data) || \is_string($data)) {
0 ignored issues
show
introduced by
The condition is_float($data) is always false.
Loading history...
introduced by
The condition is_int($data) is always false.
Loading history...
introduced by
The condition is_string($data) is always false.
Loading history...
163
		    $result = [$this->getPrimitiveMappingConfigOrThrow($data)[ RB::KEY_KEY ] => $data];
164
	    }
165
166
	    return $result;
167
    }
168
169
    /**
170
     * Recursively walks $data array and converts all known objects if found. Note
171
     * $data array is passed by reference so source $data array may be modified.
172
     *
173
     * @param array $data array to recursively convert known elements of
174
     *
175
     * @return array
176
     *
177
     * @throws \RuntimeException
178
     */
179
    protected function convertArray(array $data): array
180
    {
181
        // This is to ensure that we either have array with user provided keys i.e. ['foo'=>'bar'], which will then
182
        // be turned into JSON object or array without user specified keys (['bar']) which we would return as JSON
183
        // array. But you can't mix these two as the final JSON would not produce predictable results.
184 40
        $string_keys_cnt = 0;
185
        $int_keys_cnt = 0;
186 40
        foreach ($data as $key => $val) {
187 15
            if (\is_int($key)) {
188
                $int_keys_cnt++;
189
            } else {
190 25
                $string_keys_cnt++;
191
            }
192 25
193 25
            if (($string_keys_cnt > 0) && ($int_keys_cnt > 0)) {
194 25
                throw new \RuntimeException(
195 25
                    'Invalid data array. Either set own keys for all the items or do not specify any keys at all. ' .
196 25
                    'Arrays with mixed keys are not supported by design.');
197 25
            }
198 25
        }
199
200
        foreach ($data as $key => $val) {
201 25
            if (\is_array($val)) {
202 11
                $data[ $key ] = $this->convertArray($val);
203 10
            } elseif (\is_object($val)) {
204 10
                $cfg = $this->getClassMappingConfigOrThrow($val);
205
                $worker = new $cfg[ RB::KEY_HANDLER ]();
206
                $converted_data = $worker->convert($val, $cfg);
207 24
                $data[ $key ] = $converted_data;
208 10
            }
209
        }
210 10
211 6
        return $data;
212
    }
213 4
214
    /**
215
     * Reads and validates "converter/map" config mapping
216
     *
217 23
     * @return array Classes mapping as specified in configuration or empty array if configuration found
218 4
     *
219
     * @throws \RuntimeException if config mapping is technically invalid (i.e. not array etc).
220
     */
221 23
    protected static function getClassesMapping(): array
222
    {
223
        $classes = Config::get(RB::CONF_KEY_CONVERTER_CLASSES) ?? [];
224
225
	    if (!\is_array($classes)) {
226
		    throw new \RuntimeException(
227
			    \sprintf('CONFIG: "%s" mapping must be an array (%s given)', RB::CONF_KEY_CONVERTER_CLASSES, \gettype($classes)));
228
	    }
229
230
	    if (!empty($classes)) {
231 10
		    $mandatory_keys = [
232
			    RB::KEY_HANDLER,
233 10
			    RB::KEY_KEY,
234 10
		    ];
235 6
		    foreach ($classes as $class_name => $class_config) {
236
			    if (!\is_array($class_config)) {
237
				    throw new \InvalidArgumentException(sprintf("CONFIG: Config for '{$class_name}' class must be an array (%s given).", \gettype($class_config)));
238
			    }
239 4
			    foreach ($mandatory_keys as $key_name) {
240
				    if (!\array_key_exists($key_name, $class_config)) {
241
					    throw new \RuntimeException("CONFIG: Missing '{$key_name}' for '{$class_name}' class mapping");
242
				    }
243
			    }
244
		    }
245
	    }
246
247
        return $classes;
248
    }
249
250
	/**
251
	 * Reads and validates "converter/primitives" config mapping
252 10
	 *
253
	 * @return array Primitives mapping config as specified in configuration or empty array if configuration found
254
	 *
255
	 * @throws \RuntimeException if config mapping is technically invalid (i.e. not array etc).
256
	 */
257 10
	protected static function getPrimitivesMapping(): array
258 10
	{
259 10
		$primitives = Config::get(RB::CONF_KEY_CONVERTER_PRIMITIVES) ?? [];
260 10
261 6
		if (!\is_array($primitives)) {
262
			throw new \RuntimeException(
263 6
				\sprintf('CONFIG: "%s" mapping must be an array (%s given)', RB::CONF_KEY_CONVERTER_PRIMITIVES, \gettype($primitives)));
264
		}
265
266 10
		if (!empty($primitives)) {
267 1
			$mandatory_keys = [
268
				RB::KEY_KEY,
269 1
			];
270
271
			foreach ($primitives as $type => $config) {
272
				if (!\is_array($config)) {
273 9
					throw new \InvalidArgumentException(sprintf("CONFIG: Config for '{$type}' primitive must be an array (%s given).", \gettype($config)));
274 9
				}
275 4
				foreach ($mandatory_keys as $key_name) {
276 9
					if (!\array_key_exists($key_name, $config)) {
277 5
						throw new \RuntimeException("CONFIG: Missing '{$key_name}' for '{$type}' primitive mapping");
278 5
					}
279 5
				}
280 5
			}
281
		}
282
283
		return $primitives;
284 9
	}
285
}
286