Passed
Pull Request — dev (#170)
by
unknown
07:45
created

Converter::convert()   C

Complexity

Conditions 12
Paths 19

Size

Total Lines 38
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 12

Importance

Changes 5
Bugs 0 Features 1
Metric Value
cc 12
eloc 23
c 5
b 0
f 1
nc 19
nop 1
dl 0
loc 38
ccs 23
cts 23
cp 1
crap 12
rs 6.9666

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\Exceptions as Ex;
20
use MarcinOrlowski\ResponseBuilder\ResponseBuilder as RB;
21
22
/**
23
 * Data converter
24
 */
25
class Converter
26
{
27
	/** @var array */
28
	protected $classes = [];
29
30
	/** @var array */
31
	protected $primitives = [];
32
33
	/** @var bool */
34
	protected $debug_enabled = false;
35
36
	/**
37
	 * Converter constructor.
38
	 */
39 50
	public function __construct()
40
	{
41 50
		$this->classes = static::getClassesMapping() ?? [];
42 49
		$this->primitives = static::getPrimitivesMapping() ?? [];
43
44 47
		$this->debug_enabled = Config::get(RB::CONF_KEY_DEBUG_CONVERTER_DEBUG_ENABLED, false);
45 47
	}
46
47
	/**
48
	 * Returns "converter/primitives" entry for given primitive object or throws exception if no config found.
49
	 * Throws \RuntimeException if there's no config "classes" mapping entry for this object configured.
50
	 * Throws \InvalidArgumentException if No data conversion mapping configured for given class.
51
	 *
52
	 * @param boolean|string|double|array|int $data Primitive to get config for.
53
	 *
54
	 * @return array
55
	 *
56
	 * @throws Ex\ConfigurationNotFoundException
57
	 */
58 21
	protected function getPrimitiveMappingConfigOrThrow($data): array
59
	{
60 21
		$result = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
61
62 21
		$type = \gettype($data);
63 21
		$result = $this->primitives[ $type ] ?? null;
64
65 21
		if ($result === null) {
66 1
			throw new Ex\ConfigurationNotFoundException(
67 1
				sprintf('No data conversion mapping configured for "%s" primitive.', $type));
68
		}
69
70 20
		if ($this->debug_enabled) {
71 20
			Log::debug(__CLASS__ . ": Converting primitive type of '{$type}' to data node with key '{$result[RB::KEY_KEY]}'.");
72
		}
73
74 20
		return $result;
75
	}
76
77
	/**
78
	 * Returns "converter/map" mapping configured for given $data object class or throws exception if not found.
79
	 *
80
	 * @param object $data Object to get config for.
81
	 *
82
	 * @return array
83
	 *
84
	 * @throws Ex\ConfigurationNotFoundException
85
	 */
86 16
	protected function getClassMappingConfigOrThrow(object $data): array
87
	{
88 16
		$result = null;
89 16
		$debug_result = '';
90
91
		// check for exact class name match...
92 16
		$cls = \get_class($data);
93 16
		if (\is_string($cls)) {
0 ignored issues
show
introduced by
The condition is_string($cls) is always true.
Loading history...
94 16
			if (\array_key_exists($cls, $this->classes)) {
95 11
				$result = $this->classes[ $cls ];
96 11
				$debug_result = 'exact config match';
97
			} else {
98
				// no exact match, then lets try with `instanceof`
99 5
				foreach (\array_keys($this->classes) as $class_name) {
100 5
					if ($data instanceof $class_name) {
101 4
						$result = $this->classes[ $class_name ];
102 4
						$debug_result = "subclass of {$class_name}";
103 4
						break;
104
					}
105
				}
106
			}
107
		}
108
109 16
		if ($result === null) {
110 1
			throw new Ex\ConfigurationNotFoundException(
111 1
				sprintf('No data conversion mapping configured for "%s" class.', $cls));
112
		}
113
114 15
		if ($this->debug_enabled) {
115 15
			Log::debug(__CLASS__ . ": Converting {$cls} using {$result[RB::KEY_HANDLER]} because: {$debug_result}.");
116
		}
117
118 15
		return $result;
119
	}
120
121
	/**
122
	 * Main entry for data conversion
123
	 *
124
	 * @param object|array|null $data
125
	 *
126
	 * @return mixed|null
127
	 */
128 46
	public function convert($data = null): ?array
129
	{
130 46
		if ($data === null) {
131 15
			return null;
132
		}
133
134 31
		$result = null;
135
136 31
		Validator::assertIsType('data', $data, [
137 31
			Type::ARRAY,
138 31
			Type::BOOLEAN,
139 31
			Type::DOUBLE,
140 31
			Type::INTEGER,
141 31
			Type::OBJECT,
142 31
			Type::STRING,
143
		]);
144
145 31
		if ($result === null && \is_object($data)) {
146 10
			$cfg = $this->getClassMappingConfigOrThrow($data);
147 10
			$worker = new $cfg[ RB::KEY_HANDLER ]();
148 10
			$result = $worker->convert($data, $cfg);
149
			$result = $cfg[ RB::KEY_KEY ] === null ? $result : [$cfg[ RB::KEY_KEY ] => $result];
150
        }
151 31
152 11
		if ($result === null && \is_array($data)) {
153
			$cfg = $this->getPrimitiveMappingConfigOrThrow($data);
154 11
155 9
			$result = $this->convertArray($data);
156 4
			if (!Util::isArrayWithNonNumericKeys($data)) {
157
				$result = [$cfg[ RB::KEY_KEY ] => $result];
158
			}
159
		}
160 29
161 10
		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_string($data) is always false.
Loading history...
introduced by
The condition is_int($data) is always false.
Loading history...
162
			$result = [$this->getPrimitiveMappingConfigOrThrow($data)[ RB::KEY_KEY ] => $data];
163
		}
164 28
165
		return $result;
166
	}
167
168
	/**
169
	 * Recursively walks $data array and converts all known objects if found. Note
170
	 * $data array is passed by reference so source $data array may be modified.
171
	 *
172
	 * @param array $data array to recursively convert known elements of
173
	 *
174
	 * @return array
175 11
	 */
176
	protected function convertArray(array $data): array
177 11
	{
178
		Validator::assertArrayHasNoMixedKeys($data);
179 9
180 9
		foreach ($data as $key => $val) {
181 4
			if (\is_array($val)) {
182 9
				$data[ $key ] = $this->convertArray($val);
183 5
			} elseif (\is_object($val)) {
184 5
				$cfg = $this->getClassMappingConfigOrThrow($val);
185 5
				$worker = new $cfg[ RB::KEY_HANDLER ]();
186 5
				$converted_data = $worker->convert($val, $cfg);
187
				$data[ $key ] = $converted_data;
188
			}
189
		}
190 9
191
		return $data;
192
	}
193
194
	/**
195
	 * Reads and validates "converter/map" config mapping
196
	 *
197
	 * @return array Classes mapping as specified in configuration or empty array if configuration found
198
	 *
199
	 * @throws Ex\InvalidConfigurationException if whole config mapping is technically invalid (i.e. not an array etc).
200
	 * @throws Ex\InvalidConfigurationElementException if config for specific class is technically invalid (i.e. not an array etc).
201
	 * @throws Ex\IncompleteConfigurationException if config for specific class is incomplete (misses some mandatory fields etc).
202 55
	 */
203
	protected static function getClassesMapping(): array
204 55
	{
205
		$classes = Config::get(RB::CONF_KEY_CONVERTER_CLASSES) ?? [];
206 55
207 3
		if (!\is_array($classes)) {
208 3
			throw new Ex\InvalidConfigurationException(
209
				\sprintf('"%s" must be an array (%s found)', RB::CONF_KEY_CONVERTER_CLASSES, \gettype($classes)));
210
		}
211 52
212
		if (!empty($classes)) {
213 51
			$mandatory_keys = [
214
				RB::KEY_HANDLER => [TYPE::STRING],
215
				RB::KEY_KEY => [TYPE::STRING, TYPE::NULL],
216 51
			];
217 51
			foreach ($classes as $class_name => $class_config) {
218 1
				if (!\is_array($class_config)) {
219 1
					throw new Ex\InvalidConfigurationElementException(
220
						sprintf("Config for '{$class_name}' class must be an array (%s found).", \gettype($class_config)));
221 50
				}
222 50
				foreach ($mandatory_keys as $key_name => $allowed_types) {
223 1
					if (!\array_key_exists($key_name, $class_config)) {
224 1
						throw new Ex\IncompleteConfigurationException(
225
							"Missing '{$key_name}' entry in '{$class_name}' class mapping config.");
226
					}
227
228
                    Validator::assertIsType(RB::KEY_KEY, $class_config[$key_name], $allowed_types);
229
				}
230 50
			}
231
		}
232
233
		return $classes;
234
	}
235
236
	/**
237
	 * Reads and validates "converter/primitives" config mapping
238
	 *
239
	 * @return array Primitives mapping config as specified in configuration or empty array if configuration found
240
	 *
241
	 * @throws Ex\InvalidConfigurationException if whole config mapping is technically invalid (i.e. not an array etc).
242 51
	 * @throws Ex\InvalidConfigurationElementException if config for specific class is technically invalid (i.e. not an array etc).
243
	 * @throws Ex\IncompleteConfigurationException if config for specific class is incomplete (misses some mandatory fields etc).
244 51
	 */
245
	protected static function getPrimitivesMapping(): array
246 51
	{
247 1
		$primitives = Config::get(RB::CONF_KEY_CONVERTER_PRIMITIVES) ?? [];
248 1
249
		if (!\is_array($primitives)) {
250
			throw new Ex\InvalidConfigurationException(
251 50
				\sprintf('"%s" mapping must be an array (%s found)', RB::CONF_KEY_CONVERTER_PRIMITIVES, \gettype($primitives)));
252
		}
253 49
254
		if (!empty($primitives)) {
255
			$mandatory_keys = [
256 49
				RB::KEY_KEY,
257 49
			];
258 1
259 1
			foreach ($primitives as $type => $config) {
260
				if (!\is_array($config)) {
261 49
					throw new Ex\InvalidConfigurationElementException(
262 49
						sprintf("Config for '{$type}' primitive must be an array (%s found).", \gettype($config)));
263 1
				}
264 1
				foreach ($mandatory_keys as $key_name) {
265
					if (!\array_key_exists($key_name, $config)) {
266
						throw new Ex\IncompleteConfigurationException(
267
							"Missing '{$key_name}' entry in '{$type}' primitive mapping config.");
268
					}
269
				}
270 48
			}
271
		}
272
273
		return $primitives;
274
	}
275
}
276