Passed
Push — dev ( d2f61c...886cc9 )
by Marcin
09:09
created

Converter::getPrimitiveMappingConfigOrThrow()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 12.4085

Importance

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