Converter::getClassMappingConfigOrThrow()   B
last analyzed

Complexity

Conditions 7
Paths 15

Size

Total Lines 33
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 7.049

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 7
eloc 19
c 2
b 0
f 0
nc 15
nop 1
dl 0
loc 33
ccs 9
cts 10
cp 0.9
crap 7.049
rs 8.8333
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-2021 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 38
	public function __construct()
40
	{
41 38
		$this->classes = static::getClassesMapping() ?? [];
42
		$this->primitives = static::getPrimitivesMapping() ?? [];
43 37
44 37
		$this->debug_enabled = Config::get(RB::CONF_KEY_DEBUG_CONVERTER_DEBUG_ENABLED, false);
45
	}
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 5
	 *
52
	 * @param boolean|string|double|array|int $data Primitive to get config for.
53 5
	 *
54
	 * @return array
55
	 *
56
	 * @throws Ex\ConfigurationNotFoundException
57
	 */
58
	protected function getPrimitiveMappingConfigOrThrow($data): array
59
	{
60
		$result = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
61
62
		$type = \gettype($data);
63
		$result = $this->primitives[ $type ] ?? null;
64
65
		if ($result === null) {
66
			throw new Ex\ConfigurationNotFoundException(
67
				sprintf('No data conversion mapping configured for "%s" primitive.', $type));
68 16
		}
69
70 16
		if ($this->debug_enabled) {
71 16
			Log::debug(__CLASS__ . ": Converting primitive type of '{$type}' to data node with key '{$result[RB::KEY_KEY]}'.");
72
		}
73
74 16
		return $result;
75 16
	}
76 16
77 11
	/**
78 11
	 * 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 5
	 *
82 5
	 * @return array
83 4
	 *
84 4
	 * @throws Ex\ConfigurationNotFoundException
85 4
	 */
86
	protected function getClassMappingConfigOrThrow(object $data): array
87
	{
88
		$result = null;
89
		$debug_result = '';
90
91 16
		// check for exact class name match...
92 1
		$cls = \get_class($data);
93
		if ($cls !== false) {
94
			if (\array_key_exists($cls, $this->classes)) {
95 15
				$result = $this->classes[ $cls ];
96
				$debug_result = 'exact config match';
97
			} else {
98
				// no exact match, then lets try with `instanceof`
99 15
				foreach (\array_keys($this->classes) as $class_name) {
100
					if ($data instanceof $class_name) {
101
						$result = $this->classes[ $class_name ];
102
						$debug_result = "subclass of {$class_name}";
103
						break;
104
					}
105
				}
106
			}
107
		}
108
109
		if ($result === null) {
110
			throw new Ex\ConfigurationNotFoundException(
111 37
				sprintf('No data conversion mapping configured for "%s" class.', $cls));
112
		}
113 37
114 15
		if ($this->debug_enabled) {
115
			Log::debug(__CLASS__ . ": Converting {$cls} using {$result[RB::KEY_HANDLER]} because: {$debug_result}.");
116
		}
117 22
118 22
		return $result;
119
	}
120 21
121 11
	/**
122 10
	 * Main entry for data conversion
123 10
	 *
124
	 * @param mixed|null $data
125 10
	 *
126
	 * @return array|null
127
	 */
128 19
	public function convert($data = null): ?array
129
	{
130
		if ($data === null) {
131
			return null;
132
		}
133
134
		$result = null;
135
136
		Validator::assertIsType('data', $data, [
137
			Type::ARRAY,
138
			Type::BOOLEAN,
139
			Type::DOUBLE,
140
			Type::INTEGER,
141 10
			Type::OBJECT,
142
			Type::STRING,
143
		]);
144
145
		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 10
			$result = $cfg[ RB::KEY_KEY ] === null ? $result : [$cfg[ RB::KEY_KEY ] => $result];
150 6
        }
151
152 6
		if ($result === null && \is_array($data)) {
153
			$cfg = $this->getPrimitiveMappingConfigOrThrow($data);
154
155 10
			$result = $this->convertArray($data);
156 1
			if (!Util::isArrayWithNonNumericKeys($data)) {
157
				$result = [$cfg[ RB::KEY_KEY ] => $result];
158 1
			}
159
		}
160
161
		if (\is_bool($data) || \is_float($data) || \is_int($data) || \is_string($data)) {
162 9
			$result = [$this->getPrimitiveMappingConfigOrThrow($data)[ RB::KEY_KEY ] => $data];
163 9
		}
164 4
165 9
		return $result;
166 5
	}
167 5
168 5
	/**
169 5
	 * 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 9
	 *
174
	 * @return array
175
	 */
176
	protected function convertArray(array $data): array
177
	{
178
		Validator::assertArrayHasNoMixedKeys($data);
179
180
		foreach ($data as $key => $val) {
181
			if (\is_array($val)) {
182
				$data[ $key ] = $this->convertArray($val);
183 42
			} elseif (\is_object($val)) {
184
				$cfg = $this->getClassMappingConfigOrThrow($val);
185 42
				$worker = new $cfg[ RB::KEY_HANDLER ]();
186
				$converted_data = $worker->convert($val, $cfg);
187 42
				$data[ $key ] = $converted_data;
188 41
			}
189 3
		}
190 3
191
		return $data;
192
	}
193
194 38
	/**
195
	 * Reads and validates "converter/map" config mapping
196 38
	 *
197 38
	 * @return array Classes mapping as specified in configuration or empty array if configuration found
198 38
	 *
199 1
	 * @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
	 */
203
	protected static function getClassesMapping(): array
204 1
	{
205
		$classes = Config::get(RB::CONF_KEY_CONVERTER_CLASSES) ?? [];
206
207 38
		if (!\is_array($classes)) {
208
			throw new Ex\InvalidConfigurationException(
209
				\sprintf('"%s" must be an array (%s found)', RB::CONF_KEY_CONVERTER_CLASSES, \gettype($classes)));
210
		}
211
212
		if (!empty($classes)) {
213
			$mandatory_keys = [
214
				RB::KEY_HANDLER => [TYPE::STRING],
215
				RB::KEY_KEY => [TYPE::STRING, TYPE::NULL],
216
			];
217
			foreach ($classes as $class_name => $class_config) {
218
				if (!\is_array($class_config)) {
219
					throw new Ex\InvalidConfigurationElementException(
220
						sprintf("Config for '{$class_name}' class must be an array (%s found).", \gettype($class_config)));
221
				}
222
				foreach ($mandatory_keys as $key_name => $allowed_types) {
223
					if (!\array_key_exists($key_name, $class_config)) {
224
						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
			}
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
	 * @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
	 */
245
	protected static function getPrimitivesMapping(): array
246
	{
247
		$primitives = Config::get(RB::CONF_KEY_CONVERTER_PRIMITIVES) ?? [];
248
249
		if (!\is_array($primitives)) {
250
			throw new Ex\InvalidConfigurationException(
251
				\sprintf('"%s" mapping must be an array (%s found)', RB::CONF_KEY_CONVERTER_PRIMITIVES, \gettype($primitives)));
252
		}
253
254
		if (!empty($primitives)) {
255
			$mandatory_keys = [
256
				RB::KEY_KEY,
257
			];
258
259
			foreach ($primitives as $type => $config) {
260
				if (!\is_array($config)) {
261
					throw new Ex\InvalidConfigurationElementException(
262
						sprintf("Config for '{$type}' primitive must be an array (%s found).", \gettype($config)));
263
				}
264
				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
			}
271
		}
272
273
		return $primitives;
274
	}
275
}
276