Completed
Push — master ( bc1f52...d1acb5 )
by Marcin
20s queued 18s
created

Converter   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 184
Duplicated Lines 0 %

Test Coverage

Coverage 98.51%

Importance

Changes 9
Bugs 0 Features 0
Metric Value
eloc 69
c 9
b 0
f 0
dl 0
loc 184
ccs 66
cts 67
cp 0.9851
rs 10
wmc 26

6 Methods

Rating   Name   Duplication   Size   Complexity  
A getClasses() 0 3 1
A getClassesMapping() 0 25 6
B getClassMappingConfigOrThrow() 0 32 7
A __construct() 0 5 1
B convertArray() 0 33 8
A convert() 0 18 3
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
20
21
/**
22
 * Data converter
23
 */
24
class Converter
25
{
26
    /**
27
     * @var array
28
     */
29
    protected $classes = [];
30
31
    /** @var bool */
32
    protected $debug_enabled = false;
33
34
    /**
35
     * Converter constructor.
36
     *
37
     * @throws \RuntimeException
38
     */
39 38
    public function __construct()
40
    {
41 38
        $this->classes = static::getClassesMapping() ?? [];
42
43 37
	    $this->debug_enabled = Config::get(ResponseBuilder::CONF_KEY_CONVERTER_DEBUG_KEY, false);
44 37
    }
45
46
    /**
47
     * Returns local copy of configuration mapping for the classes.
48
     *
49
     * @return array
50
     */
51 5
    public function getClasses(): array
52
    {
53 5
        return $this->classes;
54
    }
55
56
    /**
57
     * Checks if we have "classes" mapping configured for $data object class.
58
     * Returns @true if there's valid config for this class.
59
     * Throws \RuntimeException if there's no config "classes" mapping entry for this object configured.
60
     * Throws \InvalidArgumentException if No data conversion mapping configured for given class.
61
     *
62
     * @param object $data Object to check mapping for.
63
     *
64
     * @return array
65
     *
66
     * @throws \InvalidArgumentException
67
     */
68 16
    protected function getClassMappingConfigOrThrow(object $data): array
69
    {
70 16
        $result = null;
71 16
        $debug_result = '';
72
73
        // check for exact class name match...
74 16
        $cls = \get_class($data);
75 16
        if (\is_string($cls)) {
0 ignored issues
show
introduced by
The condition is_string($cls) is always true.
Loading history...
76 16
	        if (\array_key_exists($cls, $this->classes)) {
77 11
		        $result = $this->classes[ $cls ];
78 11
		        $debug_result = 'exact config match';
79
	        } else {
80
		        // no exact match, then lets try with `instanceof`
81 5
		        foreach (\array_keys($this->getClasses()) as $class_name) {
82 5
			        if ($data instanceof $class_name) {
83 4
				        $result = $this->classes[ $class_name ];
84 4
				        $debug_result = "subclass of {$class_name}";
85 4
				        break;
86
			        }
87
		        }
88
	        }
89
        }
90
91 16
        if ($result === null) {
92 1
            throw new \InvalidArgumentException(sprintf('No data conversion mapping configured for "%s" class.', $cls));
93
        }
94
95 15
        if ($this->debug_enabled) {
96
			Log::debug(__CLASS__ . ": Converting {$cls} using {$result['handler']} because: {$debug_result}.");
97
        }
98
99 15
	    return $result;
100
    }
101
102
    /**
103
     * We need to prepare source data
104
     *
105
     * @param object|array|null $data
106
     *
107
     * @return array|null
108
     *
109
     * @throws \InvalidArgumentException
110
     */
111 37
    public function convert($data = null): ?array
112
    {
113 37
        if ($data === null) {
114 15
            return null;
115
        }
116
117 22
        Validator::assertIsType('data', $data, [Validator::TYPE_ARRAY,
118 22
                                                Validator::TYPE_OBJECT]);
119
120 21
        if (\is_object($data)) {
121 11
            $cfg = $this->getClassMappingConfigOrThrow($data);
122 10
            $worker = new $cfg[ ResponseBuilder::KEY_HANDLER ]();
123 10
            $data = $worker->convert($data, $cfg);
124
        } else {
125 10
            $data = $this->convertArray($data);
126
        }
127
128 19
        return $data;
129
    }
130
131
    /**
132
     * Recursively walks $data array and converts all known objects if found. Note
133
     * $data array is passed by reference so source $data array may be modified.
134
     *
135
     * @param array $data array to recursively convert known elements of
136
     *
137
     * @return array
138
     *
139
     * @throws \RuntimeException
140
     */
141 10
    protected function convertArray(array $data): array
142
    {
143
        // This is to ensure that we either have array with user provided keys i.e. ['foo'=>'bar'], which will then
144
        // be turned into JSON object or array without user specified keys (['bar']) which we would return as JSON
145
        // array. But you can't mix these two as the final JSON would not produce predictable results.
146 10
        $string_keys_cnt = 0;
147 10
        $int_keys_cnt = 0;
148 10
        foreach ($data as $key => $val) {
149 10
            if (\is_int($key)) {
150 6
                $int_keys_cnt++;
151
            } else {
152 6
                $string_keys_cnt++;
153
            }
154
155 10
            if (($string_keys_cnt > 0) && ($int_keys_cnt > 0)) {
156 1
                throw new \RuntimeException(
157
                    'Invalid data array. Either set own keys for all the items or do not specify any keys at all. ' .
158 1
                    'Arrays with mixed keys are not supported by design.');
159
            }
160
        }
161
162 9
        foreach ($data as $key => $val) {
163 9
            if (\is_array($val)) {
164 4
                $data[ $key ] = $this->convertArray($val);
165 9
            } elseif (\is_object($val)) {
166 5
                $cfg = $this->getClassMappingConfigOrThrow($val);
167 5
                $worker = new $cfg[ ResponseBuilder::KEY_HANDLER ]();
168 5
                $converted_data = $worker->convert($val, $cfg);
169 5
                $data[ $key ] = $converted_data;
170
            }
171
        }
172
173 9
        return $data;
174
    }
175
176
    /**
177
     * Reads and validates "classes" config mapping
178
     *
179
     * @return array Classes mapping as specified in configuration or empty array if configuration found
180
     *
181
     * @throws \RuntimeException if "classes" mapping is technically invalid (i.e. not array etc).
182
     */
183 42
    protected static function getClassesMapping(): array
184
    {
185 42
        $classes = Config::get(ResponseBuilder::CONF_KEY_CONVERTER);
186
187 42
        if ($classes !== null) {
188 41
            if (!\is_array($classes)) {
189 3
                throw new \RuntimeException(
190 3
                    \sprintf('CONFIG: "classes" mapping must be an array (%s given)', \gettype($classes)));
191
            }
192
193
            $mandatory_keys = [
194 38
                ResponseBuilder::KEY_HANDLER,
195
            ];
196 38
            foreach ($classes as $class_name => $class_config) {
197 38
                foreach ($mandatory_keys as $key_name) {
198 38
                    if (!\array_key_exists($key_name, $class_config)) {
199 1
                        throw new \RuntimeException("CONFIG: Missing '{$key_name}' for '{$class_name}' class mapping");
200
                    }
201
                }
202
            }
203
        } else {
204 1
            $classes = [];
205
        }
206
207 38
        return $classes;
208
    }
209
}
210