Completed
Push — dev ( ecb657...68c27d )
by Marcin
02:52 queued 02:49
created

Converter   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 246
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 14
Bugs 0 Features 0
Metric Value
eloc 100
c 14
b 0
f 0
dl 0
loc 246
ccs 100
cts 100
cp 1
rs 9.2
wmc 40

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
B getClassesMapping() 0 29 7
B getClassMappingConfigOrThrow() 0 33 7
B getPrimitivesMapping() 0 29 7
A convertArray() 0 16 4
B convert() 0 37 11
A getPrimitiveMappingConfigOrThrow() 0 17 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
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 = [$cfg[ RB::KEY_KEY ] => $worker->convert($data, $cfg)];
149
		}
150
151 31
		if ($result === null && \is_array($data)) {
152 11
			$cfg = $this->getPrimitiveMappingConfigOrThrow($data);
153
154 11
			$result = $this->convertArray($data);
155 9
			if (!Util::isArrayWithNonNumericKeys($data)) {
156 4
				$result = [$cfg[ RB::KEY_KEY ] => $result];
157
			}
158
		}
159
160 29
		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...
161 10
			$result = [$this->getPrimitiveMappingConfigOrThrow($data)[ RB::KEY_KEY ] => $data];
162
		}
163
164 28
		return $result;
165
	}
166
167
	/**
168
	 * Recursively walks $data array and converts all known objects if found. Note
169
	 * $data array is passed by reference so source $data array may be modified.
170
	 *
171
	 * @param array $data array to recursively convert known elements of
172
	 *
173
	 * @return array
174
	 */
175 11
	protected function convertArray(array $data): array
176
	{
177 11
		Validator::assertArrayHasNoMixedKeys($data);
178
179 9
		foreach ($data as $key => $val) {
180 9
			if (\is_array($val)) {
181 4
				$data[ $key ] = $this->convertArray($val);
182 9
			} elseif (\is_object($val)) {
183 5
				$cfg = $this->getClassMappingConfigOrThrow($val);
184 5
				$worker = new $cfg[ RB::KEY_HANDLER ]();
185 5
				$converted_data = $worker->convert($val, $cfg);
186 5
				$data[ $key ] = $converted_data;
187
			}
188
		}
189
190 9
		return $data;
191
	}
192
193
	/**
194
	 * Reads and validates "converter/map" config mapping
195
	 *
196
	 * @return array Classes mapping as specified in configuration or empty array if configuration found
197
	 *
198
	 * @throws Ex\InvalidConfigurationException if whole config mapping is technically invalid (i.e. not an array etc).
199
	 * @throws Ex\InvalidConfigurationElementException if config for specific class is technically invalid (i.e. not an array etc).
200
	 * @throws Ex\IncompleteConfigurationException if config for specific class is incomplete (misses some mandatory fields etc).
201
	 */
202 55
	protected static function getClassesMapping(): array
203
	{
204 55
		$classes = Config::get(RB::CONF_KEY_CONVERTER_CLASSES) ?? [];
205
206 55
		if (!\is_array($classes)) {
207 3
			throw new Ex\InvalidConfigurationException(
208 3
				\sprintf('"%s" must be an array (%s found)', RB::CONF_KEY_CONVERTER_CLASSES, \gettype($classes)));
209
		}
210
211 52
		if (!empty($classes)) {
212
			$mandatory_keys = [
213 51
				RB::KEY_HANDLER,
214
				RB::KEY_KEY,
215
			];
216 51
			foreach ($classes as $class_name => $class_config) {
217 51
				if (!\is_array($class_config)) {
218 1
					throw new Ex\InvalidConfigurationElementException(
219 1
						sprintf("Config for '{$class_name}' class must be an array (%s found).", \gettype($class_config)));
220
				}
221 50
				foreach ($mandatory_keys as $key_name) {
222 50
					if (!\array_key_exists($key_name, $class_config)) {
223 1
						throw new Ex\IncompleteConfigurationException(
224 1
							"Missing '{$key_name}' entry in '{$class_name}' class mapping config.");
225
					}
226
				}
227
			}
228
		}
229
230 50
		return $classes;
231
	}
232
233
	/**
234
	 * Reads and validates "converter/primitives" config mapping
235
	 *
236
	 * @return array Primitives mapping config as specified in configuration or empty array if configuration found
237
	 *
238
	 * @throws Ex\InvalidConfigurationException if whole config mapping is technically invalid (i.e. not an array etc).
239
	 * @throws Ex\InvalidConfigurationElementException if config for specific class is technically invalid (i.e. not an array etc).
240
	 * @throws Ex\IncompleteConfigurationException if config for specific class is incomplete (misses some mandatory fields etc).
241
	 */
242 51
	protected static function getPrimitivesMapping(): array
243
	{
244 51
		$primitives = Config::get(RB::CONF_KEY_CONVERTER_PRIMITIVES) ?? [];
245
246 51
		if (!\is_array($primitives)) {
247 1
			throw new Ex\InvalidConfigurationException(
248 1
				\sprintf('"%s" mapping must be an array (%s found)', RB::CONF_KEY_CONVERTER_PRIMITIVES, \gettype($primitives)));
249
		}
250
251 50
		if (!empty($primitives)) {
252
			$mandatory_keys = [
253 49
				RB::KEY_KEY,
254
			];
255
256 49
			foreach ($primitives as $type => $config) {
257 49
				if (!\is_array($config)) {
258 1
					throw new Ex\InvalidConfigurationElementException(
259 1
						sprintf("Config for '{$type}' primitive must be an array (%s found).", \gettype($config)));
260
				}
261 49
				foreach ($mandatory_keys as $key_name) {
262 49
					if (!\array_key_exists($key_name, $config)) {
263 1
						throw new Ex\IncompleteConfigurationException(
264 1
							"Missing '{$key_name}' entry in '{$type}' primitive mapping config.");
265
					}
266
				}
267
			}
268
		}
269
270 48
		return $primitives;
271
	}
272
}
273