Passed
Branch master (4ddde1)
by Marcin
09:17
created

Converter::convert()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
eloc 9
c 2
b 0
f 0
nc 4
nop 1
dl 0
loc 15
rs 9.9666
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-2019 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\Config;
18
19
20
/**
21
 * Data converter
22
 */
23
class Converter
24
{
25
	/**
26
	 * @var array|null
27
	 */
28
	protected $classes;
29
30
	/**
31
	 * Converter constructor.
32
	 */
33
	public function __construct()
34
	{
35
		$classes = Config::get(ResponseBuilder::CONF_KEY_CLASSES) ?? [];
36
		if (!is_array($classes)) {
37
			throw new \RuntimeException(
38
				sprintf('CONFIG: "classes" mapping must be an array (%s given)', gettype($classes)));
39
		}
40
41
		$this->classes = $classes;
42
	}
43
44
	/**
45
	 * Returns local copy of configuration mapping for the classes.
46
	 *
47
	 * @return array|null
48
	 */
49
	public function getClasses(): ?array
50
	{
51
		return $this->classes;
52
	}
53
54
	/**
55
	 * Checks if we have "classes" mapping configured for $data object class.
56
	 * Returns @true if there's valid config for this class.
57
	 *
58
	 * @param object $data Object to check mapping for.
59
	 *
60
	 * @return array
61
	 *
62
	 * @throws \RuntimeException if there's no config "classes" mapping entry
63
	 *                           for this object configured.
64
	 */
65
	protected function getClassMappingConfigOrThrow(object $data): array
66
	{
67
		$result = null;
68
69
		// check for exact class name match...
70
		$cls = get_class($data);
71
		if (array_key_exists($cls, $this->classes)) {
0 ignored issues
show
Bug introduced by
It seems like $this->classes can also be of type null; however, parameter $search of array_key_exists() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

71
		if (array_key_exists($cls, /** @scrutinizer ignore-type */ $this->classes)) {
Loading history...
72
			$result = $this->classes[ $cls ];
73
		} else {
74
			// no exact match, then lets try with `instanceof`
75
			foreach (array_keys($this->classes) as $class_name) {
0 ignored issues
show
Bug introduced by
It seems like $this->classes can also be of type null; however, parameter $input of array_keys() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

75
			foreach (array_keys(/** @scrutinizer ignore-type */ $this->classes) as $class_name) {
Loading history...
76
				if ($data instanceof $class_name) {
77
					$result = $this->classes[ $class_name ];
78
					break;
79
				}
80
			}
81
		}
82
83
		if ($result === null) {
84
			throw new \InvalidArgumentException(sprintf('No data conversion mapping configured for "%s" class.', $cls));
85
		}
86
87
		return $result;
88
	}
89
90
	/**
91
	 * We need to prepare source data
92
	 *
93
	 * @param null $data
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $data is correct as it would always require null to be passed?
Loading history...
94
	 *
95
	 * @return array|null
96
	 */
97
	public function convert($data = null): ?array
98
	{
99
		if ($data === null) {
0 ignored issues
show
introduced by
The condition $data === null is always true.
Loading history...
100
			return null;
101
		}
102
103
		if (is_object($data)) {
104
			$cfg = $this->getClassMappingConfigOrThrow($data);
105
			$data = [$cfg[ ResponseBuilder::KEY_KEY ] => $data->{$cfg[ ResponseBuilder::KEY_METHOD ]}()];
106
		} elseif (!is_array($data)) {
107
			throw new \InvalidArgumentException(
108
				sprintf('Invalid payload data. Must be null, array or object with mapping ("%s" given).', gettype($data)));
109
		}
110
111
		return $this->convertArray($data);
112
	}
113
114
	/**
115
	 * Recursively walks $data array and converts all known objects if found. Note
116
	 * $data array is passed by reference so source $data array may be modified.
117
	 *
118
	 * @param array $data array to recursively convert known elements of
119
	 *
120
	 * @return array
121
	 */
122
	protected function convertArray(array $data): array
123
	{
124
		// This is to ensure that we either have array with user provided keys i.e. ['foo'=>'bar'], which will then
125
		// be turned into JSON object or array without user specified keys (['bar']) which we would return as JSON
126
		// array. But you can't mix these two as the final JSON would not produce predictable results.
127
		$string_keys_cnt = 0;
128
		$int_keys_cnt = 0;
129
		foreach ($data as $key => $val) {
130
			if (is_int($key)) {
131
				$int_keys_cnt++;
132
			} else {
133
				$string_keys_cnt++;
134
			}
135
136
			if (($string_keys_cnt > 0) && ($int_keys_cnt > 0)) {
137
				throw new \RuntimeException(
138
					'Invalid data array. Either set own keys for all the items or do not specify any keys at all. ' .
139
					'Arrays with mixed keys are not supported by design.');
140
			}
141
		}
142
143
		foreach ($data as $key => $val) {
144
			if (is_array($val)) {
145
				$data[ $key ] = $this->convertArray($val);
146
			} elseif (is_object($val)) {
147
				$cls = get_class($val);
148
				if (array_key_exists($cls, $this->classes)) {
0 ignored issues
show
Bug introduced by
It seems like $this->classes can also be of type null; however, parameter $search of array_key_exists() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

148
				if (array_key_exists($cls, /** @scrutinizer ignore-type */ $this->classes)) {
Loading history...
149
					$conversion_method = $this->classes[ $cls ][ ResponseBuilder::KEY_METHOD ];
150
					$converted_data = $val->$conversion_method();
151
					$data[ $key ] = $converted_data;
152
				}
153
			}
154
		}
155
156
		return $data;
157
	}
158
}
159