DtoPropertiesMapper   A
last analyzed

Complexity

Total Complexity 32

Size/Duplication

Total Lines 281
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 78
dl 0
loc 281
ccs 79
cts 79
cp 1
rs 9.84
c 5
b 0
f 0
wmc 32

11 Methods

Rating   Name   Duplication   Size   Complexity  
A reflectDto() 0 6 2
A for() 0 3 1
A __construct() 0 4 1
A getPropertyKeyFromData() 0 7 2
A checkUnknownProperties() 0 4 3
A cacheUseStatements() 0 27 5
A getNames() 0 5 1
A map() 0 26 5
A cacheRawPropertiesOfReflection() 0 15 5
A parseTypes() 0 22 5
A cacheRawProperties() 0 7 2
1
<?php
2
3
namespace Cerbero\Dto;
4
5
use Cerbero\Dto\Exceptions\DtoNotFoundException;
6
use Cerbero\Dto\Exceptions\MissingValueException;
7
use Cerbero\Dto\Exceptions\UnknownDtoPropertyException;
8
use Cerbero\Dto\Manipulators\ArrayConverter;
9
use ReflectionClass;
10
use ReflectionException;
11
12
/**
13
 * The DTO properties mapper.
14
 *
15
 */
16
class DtoPropertiesMapper
17
{
18
    /**
19
     * The property doc comment pattern
20
     *
21
     * - Start with "@property" or "@property-read"
22
     * - Capture property type with possible "[]" suffix
23
     * - Capture variable name "$foo" or "foo"
24
     */
25
    protected const RE_PROPERTY = '/@property(?:-read)?\s+((?:[\w\\\_]+(?:\[])?\|?)+)\s+\$?([\w_]+)/';
26
27
    /**
28
     * The "use" statement pattern
29
     *
30
     * - Capture fully qualified class name
31
     * - Capture possible class alias after "as"
32
     */
33
    protected const RE_USE_STATEMENT = '/([\w\\\_]+)(?:\s+as\s+([\w_]+))?;/i';
34
35
    /**
36
     * The DTO properties mapper instances.
37
     *
38
     * @var DtoPropertiesMapper[]
39
     */
40
    protected static $instances = [];
41
42
    /**
43
     * The DTO class to map properties for.
44
     *
45
     * @var string
46
     */
47
    protected $dtoClass;
48
49
    /**
50
     * The reflection of the DTO class.
51
     *
52
     * @var ReflectionClass
53
     */
54
    protected $reflection;
55
56
    /**
57
     * The cached raw properties.
58
     *
59
     * @var array
60
     */
61
    protected $rawProperties;
62
63
    /**
64
     * The cached use statements.
65
     *
66
     * @var array
67
     */
68
    protected $useStatements;
69
70
    /**
71
     * The cached mapped properties.
72
     *
73
     * @var array
74
     */
75
    protected $mappedProperties;
76
77
    /**
78
     * Instantiate the class.
79
     *
80
     * @param  string  $dtoClass
81
     * @throws DtoNotFoundException
82
     */
83 15
    protected function __construct(string $dtoClass)
84
    {
85 15
        $this->dtoClass = $dtoClass;
86 15
        $this->reflection = $this->reflectDto();
87 14
    }
88
89
    /**
90
     * Retrieve the reflection of the given DTO class
91
     *
92
     * @return ReflectionClass
93
     * @throws DtoNotFoundException
94
     */
95 15
    protected function reflectDto(): ReflectionClass
96
    {
97
        try {
98 15
            return new ReflectionClass($this->dtoClass);
99 1
        } catch (ReflectionException $e) {
100 1
            throw new DtoNotFoundException($this->dtoClass);
101
        }
102
    }
103
104
    /**
105
     * Retrieve the mapper instance for the given DTO class
106
     *
107
     * @param  string  $dtoClass
108
     * @return DtoPropertiesMapper
109
     * @throws DtoNotFoundException
110
     */
111 89
    public static function for(string $dtoClass): DtoPropertiesMapper
112
    {
113 89
        return static::$instances[$dtoClass] = static::$instances[$dtoClass] ?? new static($dtoClass);
114
    }
115
116
    /**
117
     * Retrieve the mapped property names
118
     *
119
     * @return array
120
     */
121 1
    public function getNames(): array
122
    {
123 1
        $rawProperties = $this->cacheRawProperties();
124
125 1
        return array_keys($rawProperties);
126
    }
127
128
    /**
129
     * Retrieve and cache the raw properties to map
130
     *
131
     * @return array
132
     */
133 87
    protected function cacheRawProperties(): array
134
    {
135 87
        if (!isset($this->rawProperties)) {
136 13
            $this->cacheRawPropertiesOfReflection($this->reflection);
137
        }
138
139 87
        return $this->rawProperties;
140
    }
141
142
    /**
143
     * Cache the raw properties of the given reflection
144
     *
145
     * @param  ReflectionClass  $reflection
146
     * @return void
147
     */
148 13
    protected function cacheRawPropertiesOfReflection(ReflectionClass $reflection): void
149
    {
150 13
        $this->rawProperties = $this->rawProperties ?? [];
151
152 13
        if (preg_match_all(static::RE_PROPERTY, $reflection->getDocComment(), $matches, PREG_SET_ORDER) !== 0) {
153 11
            foreach ($matches as $match) {
154 11
                [, $rawTypes, $name] = $match;
155 11
                $this->rawProperties[$name] = $rawTypes;
156
            }
157
        }
158
159 13
        $parentDto = $reflection->getParentClass();
160
161 13
        if ($parentDto !== false && $parentDto != Dto::class) {
162 13
            $this->cacheRawPropertiesOfReflection($parentDto);
163
        }
164 13
    }
165
166
    /**
167
     * Retrieve the mapped DTO properties
168
     *
169
     * @param  array  $data
170
     * @param  int  $flags
171
     * @return array
172
     * @throws MissingValueException
173
     * @throws UnexpectedValueException
174
     * @throws UnknownDtoPropertyException
175
     */
176 86
    public function map(array $data, int $flags): array
177
    {
178 86
        $mappedProperties = [];
179 86
        $rawProperties = $this->cacheRawProperties();
180 86
        $useStatements = $this->cacheUseStatements();
181
182 86
        foreach ($rawProperties as $name => $rawTypes) {
183 77
            $cachedProperty = $this->mappedProperties[$name] ?? null;
184 77
            $types = $cachedProperty ? $cachedProperty->getTypes() : $this->parseTypes($rawTypes, $useStatements);
185 77
            $key = $this->getPropertyKeyFromData($name, $data);
186
187 77
            if (!array_key_exists($key, $data)) {
188 73
                if ($flags & PARTIAL) {
189 72
                    continue;
190
                }
191
192 1
                throw new MissingValueException($this->dtoClass, $name);
193
            }
194
195 67
            $mappedProperties[$name] = DtoProperty::create($name, $data[$key], $types, $flags);
196 67
            unset($data[$key]);
197
        }
198
199 85
        $this->checkUnknownProperties($data, $flags);
200
201 85
        return $this->mappedProperties = $mappedProperties;
202
    }
203
204
    /**
205
     * Retrieve and cache the DTO "use" statements
206
     *
207
     * @return array
208
     */
209 86
    protected function cacheUseStatements(): array
210
    {
211 86
        if (isset($this->useStatements)) {
212 76
            return $this->useStatements;
213
        }
214
215 12
        $this->useStatements = [];
216 12
        $rawStatements = null;
217 12
        $handle = fopen($this->reflection->getFileName(), 'rb');
218
219
        do {
220 12
            $line = trim(fgets($handle, 120));
221 12
            $begin = substr($line, 0, 3);
222 12
            $rawStatements .= $begin == 'use' ? $line : null;
223 12
        } while ($begin != '/**');
224
225 12
        fclose($handle);
226
227 12
        preg_match_all(static::RE_USE_STATEMENT, $rawStatements, $useMatches, PREG_SET_ORDER);
228
229 12
        foreach ($useMatches as $match) {
230 12
            $segments = explode('\\', $match[1]);
231 12
            $name = $match[2] ?? end($segments);
232 12
            $this->useStatements[$name] = $match[1];
233
        }
234
235 12
        return $this->useStatements;
236
    }
237
238
    /**
239
     * Parse the given raw property types
240
     *
241
     * @param  string  $rawTypes
242
     * @param  array  $useStatements
243
     * @return DtoPropertyTypes
244
     */
245 76
    protected function parseTypes(string $rawTypes, array $useStatements): DtoPropertyTypes
246
    {
247 76
        return array_reduce(explode('|', $rawTypes), function (DtoPropertyTypes $types, $rawType) use ($useStatements) {
248 76
            $name = str_replace('[]', '', $rawType, $count);
249 76
            $isCollection = $count > 0;
250
251
            // fully qualified class name exists
252 76
            if (strpos($rawType, '\\') === 0 && class_exists($name)) {
253 20
                return $types->addType(new DtoPropertyType(substr($name, 1), $isCollection));
254
            }
255
256 76
            if (isset($useStatements[$name])) {
257 18
                return $types->addType(new DtoPropertyType($useStatements[$name], $isCollection));
258
            }
259
260
            // class in DTO namespace exists
261 76
            if (class_exists($class = $this->reflection->getNamespaceName() . '\\' . $name)) {
262 67
                return $types->addType(new DtoPropertyType($class, $isCollection));
263
            }
264
265 76
            return $types->addType(new DtoPropertyType($name, $isCollection));
266 76
        }, new DtoPropertyTypes());
267
    }
268
269
    /**
270
     * Retrieve the key for the given property in the provided data
271
     *
272
     * @param  string  $property
273
     * @param  array  $data
274
     * @return string
275
     */
276 77
    protected function getPropertyKeyFromData(string $property, array $data): string
277
    {
278 77
        if (array_key_exists($property, $data)) {
279 66
            return $property;
280
        }
281
282 74
        return ArrayConverter::instance()->formatKey($property, true);
283
    }
284
285
    /**
286
     * Check whether the given data contains unknown properties
287
     *
288
     * @param  array  $data
289
     * @param  int  $flags
290
     * @return void
291
     * @throws UnknownDtoPropertyException
292
     */
293 85
    protected function checkUnknownProperties(array $data, int $flags): void
294
    {
295 85
        if ($data && !($flags & IGNORE_UNKNOWN_PROPERTIES)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
296 2
            throw new UnknownDtoPropertyException($this->dtoClass, key($data));
297
        }
298 85
    }
299
}
300