Completed
Push — master ( 277a70...42ae0f )
by Marcin
24s queued 11s
created

Converter::convertArray()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

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