Passed
Push — master ( 9d7ef7...af08db )
by Andrea Marco
01:42 queued 11s
created

DtoPropertiesMapper::getNames()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 1
rs 10
1
<?php
2
3
namespace Cerbero\Dto;
4
5
use Cerbero\Dto\Exceptions\DtoNotFoundException;
6
use Cerbero\Dto\Exceptions\InvalidDocCommentException;
7
use Cerbero\Dto\Exceptions\MissingValueException;
8
use Cerbero\Dto\Exceptions\UnknownDtoPropertyException;
9
use Cerbero\Dto\Manipulators\ArrayConverter;
10
use ReflectionClass;
11
use ReflectionException;
12
13
/**
14
 * The DTO properties mapper.
15
 *
16
 */
17
class DtoPropertiesMapper
18
{
19
    /**
20
     * The property doc comment pattern
21
     *
22
     * - Start with "@property" or "@property-read"
23
     * - Capture property type with possible "[]" suffix
24
     * - Capture variable name "$foo" or "foo"
25
     */
26
    protected const RE_PROPERTY = '/@property(?:-read)?\s+((?:[\w\\\_]+(?:\[])?\|?)+)\s+\$?([\w_]+)/';
27
28
    /**
29
     * The "use" statement pattern
30
     *
31
     * - Capture fully qualified class name
32
     * - Capture possible class alias after "as"
33
     */
34
    protected const RE_USE_STATEMENT = '/([\w\\\_]+)(?:\s+as\s+([\w_]+))?;/i';
35
36
    /**
37
     * The DTO properties mapper instances.
38
     *
39
     * @var DtoPropertiesMapper[]
40
     */
41
    protected static $instances = [];
42
43
    /**
44
     * The DTO class to map properties for.
45
     *
46
     * @var string
47
     */
48
    protected $dtoClass;
49
50
    /**
51
     * The reflection of the DTO class.
52
     *
53
     * @var ReflectionClass
54
     */
55
    protected $reflection;
56
57
    /**
58
     * The cached raw properties.
59
     *
60
     * @var array
61
     */
62
    protected $rawProperties;
63
64
    /**
65
     * The cached use statements.
66
     *
67
     * @var array
68
     */
69
    protected $useStatements;
70
71
    /**
72
     * The cached mapped properties.
73
     *
74
     * @var array
75
     */
76
    protected $mappedProperties;
77
78
    /**
79
     * Instantiate the class.
80
     *
81
     * @param string $dtoClass
82
     * @throws DtoNotFoundException
83
     */
84 45
    protected function __construct(string $dtoClass)
85
    {
86 45
        $this->dtoClass = $dtoClass;
87 45
        $this->reflection = $this->reflectDto();
88 42
    }
89
90
    /**
91
     * Retrieve the reflection of the given DTO class
92
     *
93
     * @return ReflectionClass
94
     * @throws DtoNotFoundException
95
     */
96 45
    protected function reflectDto(): ReflectionClass
97
    {
98
        try {
99 45
            return new ReflectionClass($this->dtoClass);
100 3
        } catch (ReflectionException $e) {
101 3
            throw new DtoNotFoundException($this->dtoClass);
102
        }
103
    }
104
105
    /**
106
     * Retrieve the mapper instance for the given DTO class
107
     *
108
     * @param string $dtoClass
109
     * @return DtoPropertiesMapper
110
     * @throws DtoNotFoundException
111
     */
112 264
    public static function for(string $dtoClass): DtoPropertiesMapper
113
    {
114 264
        return static::$instances[$dtoClass] = static::$instances[$dtoClass] ?? new static($dtoClass);
115
    }
116
117
    /**
118
     * Retrieve the mapped property names
119
     *
120
     * @return array
121
     * @throws InvalidDocCommentException
122
     */
123 3
    public function getNames(): array
124
    {
125 3
        $rawProperties = $this->cacheRawProperties();
126
127 3
        return array_keys($rawProperties);
128
    }
129
130
    /**
131
     * Retrieve and cache the raw properties to map
132
     *
133
     * @return array
134
     * @throws InvalidDocCommentException
135
     */
136 258
    protected function cacheRawProperties(): array
137
    {
138 258
        if (isset($this->rawProperties)) {
139 225
            return $this->rawProperties;
140
        }
141
142 39
        if (false === $docComment = $this->reflection->getDocComment()) {
143 3
            throw new InvalidDocCommentException($this->dtoClass);
144
        }
145
146 36
        if (preg_match_all(static::RE_PROPERTY, $docComment, $matches, PREG_SET_ORDER) === 0) {
0 ignored issues
show
Unused Code introduced by
The call to preg_match_all() has too many arguments starting with Cerbero\Dto\PREG_SET_ORDER. ( Ignorable by Annotation )

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

146
        if (/** @scrutinizer ignore-call */ preg_match_all(static::RE_PROPERTY, $docComment, $matches, PREG_SET_ORDER) === 0) {

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
147 15
            return $this->rawProperties = [];
148
        }
149
150 30
        foreach ($matches as $match) {
151 30
            [, $rawTypes, $name] = $match;
152 30
            $this->rawProperties[$name] = $rawTypes;
153
        }
154
155 30
        return $this->rawProperties;
156
    }
157
158
    /**
159
     * Retrieve the mapped DTO properties
160
     *
161
     * @param array $data
162
     * @param int $flags
163
     * @return array
164
     * @throws InvalidDocCommentException
165
     * @throws MissingValueException
166
     * @throws UnexpectedValueException
167
     * @throws UnknownDtoPropertyException
168
     */
169 255
    public function map(array $data, int $flags): array
170
    {
171 255
        $mappedProperties = [];
172 255
        $rawProperties = $this->cacheRawProperties();
173 252
        $useStatements = $this->cacheUseStatements();
174
175 252
        foreach ($rawProperties as $name => $rawTypes) {
176 225
            $cachedProperty = $this->mappedProperties[$name] ?? null;
177 225
            $types = $cachedProperty ? $cachedProperty->getTypes() : $this->parseTypes($rawTypes, $useStatements);
178 225
            $key = $this->getPropertyKeyFromData($name, $data);
179
180 225
            if (!array_key_exists($key, $data)) {
181 222
                if ($types->haveDefaultValue($flags)) {
182 15
                    $data[$key] = $types->getDefaultValue($flags);
183 222
                } elseif ($flags & PARTIAL) {
184 219
                    continue;
185
                } else {
186 3
                    throw new MissingValueException($this->dtoClass, $name);
187
                }
188
            }
189
190 195
            $mappedProperties[$name] = DtoProperty::create($name, $data[$key], $types, $flags);
191 195
            unset($data[$key]);
192
        }
193
194 249
        $this->checkUnknownProperties($data, $flags);
195
196 249
        return $this->mappedProperties = $mappedProperties;
197
    }
198
199
    /**
200
     * Retrieve and cache the DTO "use" statements
201
     *
202
     * @return array
203
     */
204 252
    protected function cacheUseStatements(): array
205
    {
206 252
        if (isset($this->useStatements)) {
207 225
            return $this->useStatements;
208
        }
209
210 33
        $this->useStatements = [];
211 33
        $rawStatements = null;
212 33
        $handle = fopen($this->reflection->getFileName(), 'rb');
213
214
        do {
215 33
            $line = trim(fgets($handle, 120));
0 ignored issues
show
Bug introduced by
It seems like $handle can also be of type false; however, parameter $handle of fgets() does only seem to accept resource, 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

215
            $line = trim(fgets(/** @scrutinizer ignore-type */ $handle, 120));
Loading history...
216 33
            $begin = substr($line, 0, 3);
217 33
            $rawStatements .= $begin == 'use' ? $line : null;
218 33
        } while ($begin != '/**');
219
220 33
        fclose($handle);
0 ignored issues
show
Bug introduced by
It seems like $handle can also be of type false; however, parameter $handle of fclose() does only seem to accept resource, 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

220
        fclose(/** @scrutinizer ignore-type */ $handle);
Loading history...
221
222 33
        preg_match_all(static::RE_USE_STATEMENT, $rawStatements, $useMatches, PREG_SET_ORDER);
223
224 33
        foreach ($useMatches as $match) {
225 33
            $segments = explode('\\', $match[1]);
226 33
            $name = $match[2] ?? end($segments);
227 33
            $this->useStatements[$name] = $match[1];
228
        }
229
230 33
        return $this->useStatements;
231
    }
232
233
    /**
234
     * Parse the given raw property types
235
     *
236
     * @param string $rawTypes
237
     * @param array $useStatements
238
     * @return DtoPropertyTypes
239
     */
240 150
    protected function parseTypes(string $rawTypes, array $useStatements): DtoPropertyTypes
241
    {
242 75
        return array_reduce(explode('|', $rawTypes), function (DtoPropertyTypes $types, $rawType) use ($useStatements) {
243 225
            $name = str_replace('[]', '', $rawType, $count);
244 225
            $isCollection = $count > 0;
245
246
            // fully qualified class name exists
247 225
            if (strpos($rawType, '\\') === 0 && class_exists($name)) {
248 57
                return $types->addType(new DtoPropertyType(substr($name, 1), $isCollection));
249
            }
250
251 225
            if (isset($useStatements[$name])) {
252 57
                return $types->addType(new DtoPropertyType($useStatements[$name], $isCollection));
253
            }
254
255
            // class in DTO namespace exists
256 225
            if (class_exists($class = $this->reflection->getNamespaceName() . '\\' . $name)) {
257 204
                return $types->addType(new DtoPropertyType($class, $isCollection));
258
            }
259
260 225
            return $types->addType(new DtoPropertyType($name, $isCollection));
261 225
        }, new DtoPropertyTypes());
262
    }
263
264
    /**
265
     * Retrieve the key for the given property in the provided data
266
     *
267
     * @param string $property
268
     * @param array $data
269
     * @return string
270
     */
271 225
    protected function getPropertyKeyFromData(string $property, array $data): string
272
    {
273 225
        if (array_key_exists($property, $data)) {
274 189
            return $property;
275
        }
276
277 225
        return strtolower(preg_replace(ArrayConverter::RE_SNAKE_CASE, '_', $property));
278
    }
279
280
    /**
281
     * Check whether the given data contains unknown properties
282
     *
283
     * @param array $data
284
     * @param int $flags
285
     * @return void
286
     * @throws UnknownDtoPropertyException
287
     */
288 249
    protected function checkUnknownProperties(array $data, int $flags): void
289
    {
290 249
        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...
291 6
            throw new UnknownDtoPropertyException($this->dtoClass, key($data));
292
        }
293 249
    }
294
}
295