Passed
Push — master ( 0995a0...acd513 )
by Andrea Marco
01:40 queued 11s
created

DtoPropertiesMapper::checkUnknownProperties()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 2
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 3
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 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 33
    protected function __construct(string $dtoClass)
84
    {
85 33
        $this->dtoClass = $dtoClass;
86 33
        $this->reflection = $this->reflectDto();
87 30
    }
88
89
    /**
90
     * Retrieve the reflection of the given DTO class
91
     *
92
     * @return ReflectionClass
93
     * @throws DtoNotFoundException
94
     */
95 33
    protected function reflectDto(): ReflectionClass
96
    {
97
        try {
98 33
            return new ReflectionClass($this->dtoClass);
99 3
        } catch (ReflectionException $e) {
100 3
            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 213
    public static function for(string $dtoClass): DtoPropertiesMapper
112
    {
113 213
        return static::$instances[$dtoClass] = static::$instances[$dtoClass] ?? new static($dtoClass);
114
    }
115
116
    /**
117
     * Retrieve the mapped DTO properties
118
     *
119
     * @param array $data
120
     * @param int $flags
121
     * @return array
122
     * @throws InvalidDocCommentException
123
     * @throws MissingValueException
124
     * @throws UnexpectedValueException
125
     * @throws UnknownDtoPropertyException
126
     */
127 207
    public function map(array $data, int $flags): array
128
    {
129 207
        $mappedProperties = [];
130 207
        $rawProperties = $this->cacheRawProperties();
131 204
        $useStatements = $this->cacheUseStatements();
132
133 204
        foreach ($rawProperties as $name => $rawTypes) {
134 174
            $cachedProperty = $this->mappedProperties[$name] ?? null;
135 174
            $types = $cachedProperty ? $cachedProperty->getTypes() : $this->parseTypes($rawTypes, $useStatements);
136
137 174
            if (!array_key_exists($name, $data)) {
138 168
                if ($types->haveDefaultValue($flags)) {
139 12
                    $data[$name] = $types->getDefaultValue($flags);
140 162
                } elseif ($flags & PARTIAL) {
141 159
                    continue;
142
                } else {
143 3
                    throw new MissingValueException($this->dtoClass, $name);
144
                }
145
            }
146
147 168
            $mappedProperties[$name] = DtoProperty::create($name, $data[$name], $types, $flags);
148 168
            unset($data[$name]);
149
        }
150
151 201
        $this->checkUnknownProperties($data, $flags);
152
153 201
        return $this->mappedProperties = $mappedProperties;
154
    }
155
156
    /**
157
     * Retrieve and cache the raw properties to map
158
     *
159
     * @return array
160
     * @throws InvalidDocCommentException
161
     */
162 207
    protected function cacheRawProperties(): array
163
    {
164 207
        if (isset($this->rawProperties)) {
165 183
            return $this->rawProperties;
166
        }
167
168 27
        if (false === $docComment = $this->reflection->getDocComment()) {
169 3
            throw new InvalidDocCommentException($this->dtoClass);
170
        }
171
172 24
        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

172
        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...
173 12
            return $this->rawProperties = [];
174
        }
175
176 21
        foreach ($matches as $match) {
177 21
            [, $rawTypes, $name] = $match;
178 21
            $this->rawProperties[$name] = $rawTypes;
179
        }
180
181 21
        return $this->rawProperties;
182
    }
183
184
    /**
185
     * Retrieve and cache the DTO "use" statements
186
     *
187
     * @return array
188
     */
189 204
    protected function cacheUseStatements(): array
190
    {
191 204
        if (isset($this->useStatements)) {
192 183
            return $this->useStatements;
193
        }
194
195 24
        $this->useStatements = [];
196 24
        $rawStatements = null;
197 24
        $handle = fopen($this->reflection->getFileName(), 'rb');
198
199
        do {
200 24
            $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

200
            $line = trim(fgets(/** @scrutinizer ignore-type */ $handle, 120));
Loading history...
201 24
            $begin = substr($line, 0, 3);
202 24
            $rawStatements .= $begin == 'use' ? $line : null;
203 24
        } while ($begin != '/**');
204
205 24
        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

205
        fclose(/** @scrutinizer ignore-type */ $handle);
Loading history...
206
207 24
        preg_match_all(static::RE_USE_STATEMENT, $rawStatements, $useMatches, PREG_SET_ORDER);
208
209 24
        foreach ($useMatches as $match) {
210 24
            $segments = explode('\\', $match[1]);
211 24
            $name = $match[2] ?? end($segments);
212 24
            $this->useStatements[$name] = $match[1];
213
        }
214
215 24
        return $this->useStatements;
216
    }
217
218
    /**
219
     * Parse the given raw property types
220
     *
221
     * @param string $rawTypes
222
     * @param array $useStatements
223
     * @return DtoPropertyTypes
224
     */
225 174
    protected function parseTypes(string $rawTypes, array $useStatements): DtoPropertyTypes
226
    {
227
        return array_reduce(explode('|', $rawTypes), function (DtoPropertyTypes $types, $rawType) use ($useStatements) {
228 174
            $name = str_replace('[]', '', $rawType, $count);
229 174
            $isCollection = $count > 0;
230
231
            // fully qualified class name exists
232 174
            if (strpos($rawType, '\\') === 0 && class_exists($name)) {
233 54
                return $types->addType(new DtoPropertyType(substr($name, 1), $isCollection));
234
            }
235
236 174
            if (isset($useStatements[$name])) {
237 54
                return $types->addType(new DtoPropertyType($useStatements[$name], $isCollection));
238
            }
239
240
            // class in DTO namespace exists
241 174
            if (class_exists($class = $this->reflection->getNamespaceName() . '\\' . $name)) {
242 159
                return $types->addType(new DtoPropertyType($class, $isCollection));
243
            }
244
245 174
            return $types->addType(new DtoPropertyType($name, $isCollection));
246 174
        }, new DtoPropertyTypes());
247
    }
248
249
    /**
250
     * Check whether the given data contains unknown properties
251
     *
252
     * @param array $data
253
     * @param int $flags
254
     * @return void
255
     * @throws UnknownDtoPropertyException
256
     */
257 201
    protected function checkUnknownProperties(array $data, int $flags): void
258
    {
259 201
        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...
260 3
            throw new UnknownDtoPropertyException($this->dtoClass, key($data));
261
        }
262 201
    }
263
}
264