Completed
Push — master ( bb6c14...7b9d4f )
by Marcin
23s queued 11s
created

Converter::getClassesMapping()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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