Passed
Push — master ( 7a440c...724ebd )
by Marcin
10:28 queued 11s
created

Converter   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 176
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 7
Bugs 0 Features 0
Metric Value
eloc 65
c 7
b 0
f 0
dl 0
loc 176
ccs 51
cts 51
cp 1
rs 10
wmc 27

6 Methods

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