Passed
Branch feature/first-implementation (a1d316)
by Andrea Marco
02:22
created

DtoPropertiesMapper::parseTypes()   A

Complexity

Conditions 5
Paths 1

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 11
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 22
ccs 0
cts 11
cp 0
crap 30
rs 9.6111
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 ReflectionClass;
9
use ReflectionException;
10
11
/**
12
 * The DTO properties mapper.
13
 *
14
 */
15
class DtoPropertiesMapper
16
{
17
    /**
18
     * The property doc comment pattern
19
     *
20
     * - Start with "@property" or "@property-read"
21
     * - Capture property type with possible "[]" suffix
22
     * - Capture variable name "$foo" or "foo"
23
     */
24
    protected const RE_PROPERTY = '/@property(?:-read)?\s+((?:[\w\\\_]+(?:\[])?\|?)+)\s+\$?([\w_]+)/';
25
26
    /**
27
     * The "use" statement pattern
28
     *
29
     * - Capture fully qualified class name
30
     * - Capture possible class alias after "as"
31
     */
32
    protected const RE_USE_STATEMENT = '/([\w\\\_]+)(?:\s+as\s+([\w_]+))?;/i';
33
34
    /**
35
     * The DTO properties mapper instances.
36
     *
37
     * @var DtoPropertiesMapper[]
38
     */
39
    protected static $instances = [];
40
41
    /**
42
     * The DTO class to map properties for.
43
     *
44
     * @var string
45
     */
46
    protected $dtoClass;
47
48
    /**
49
     * The reflection of the DTO class.
50
     *
51
     * @var ReflectionClass
52
     */
53
    protected $reflection;
54
55
    /**
56
     * The cached raw properties.
57
     *
58
     * @var array
59
     */
60
    protected $rawProperties;
61
62
    /**
63
     * The cached use statements.
64
     *
65
     * @var array
66
     */
67
    protected $useStatements;
68
69
    /**
70
     * The cached mapped properties.
71
     *
72
     * @var array
73
     */
74
    protected $mappedProperties;
75
76
    /**
77
     * Instantiate the class.
78
     *
79
     * @param string $dtoClass
80
     * @throws DtoNotFoundException
81
     */
82 9
    protected function __construct(string $dtoClass)
83
    {
84 9
        $this->dtoClass = $dtoClass;
85 9
        $this->reflection = $this->reflectDto();
86
    }
87
88
    /**
89
     * Retrieve the reflection of the given DTO class
90
     *
91
     * @return ReflectionClass
92
     * @throws DtoNotFoundException
93
     */
94 9
    protected function reflectDto(): ReflectionClass
95
    {
96
        try {
97 9
            return new ReflectionClass($this->dtoClass);
98 9
        } catch (ReflectionException $e) {
99 9
            throw new DtoNotFoundException($this->dtoClass);
100
        }
101
    }
102
103
    /**
104
     * Retrieve the mapper instance for the given DTO class
105
     *
106
     * @param string $dtoClass
107
     * @return DtoPropertiesMapper
108
     * @throws DtoNotFoundException
109
     */
110 9
    public static function for(string $dtoClass): DtoPropertiesMapper
111
    {
112 9
        return static::$instances[$dtoClass] = static::$instances[$dtoClass] ?? new static($dtoClass);
113
    }
114
115
    /**
116
     * Retrieve the mapped DTO properties
117
     *
118
     * @param array $data
119
     * @param int $flags
120
     * @return array
121
     * @throws MissingValueException
122
     * @throws UnexpectedValueException
123
     */
124
    public function map(array $data, int $flags): array
125
    {
126
        $mappedProperties = [];
127
        $rawProperties = $this->cacheRawProperties();
128
        $useStatements = $this->cacheUseStatements();
129
130
        foreach ($rawProperties as $name => $rawTypes) {
131
            $cachedProperty = $this->mappedProperties[$name] ?? null;
132
            $types = $cachedProperty ? $cachedProperty->getTypes() : $this->parseTypes($rawTypes, $useStatements);
133
134
            if (!array_key_exists($name, $data)) {
135
                if ($types->haveDefaultValue($flags)) {
136
                    $data[$name] = $types->getDefaultValue($flags);
137
                } elseif ($flags & PARTIAL) {
138
                    continue;
139
                } else {
140
                    throw new MissingValueException($this->dtoClass, $name);
141
                }
142
            }
143
144
            $mappedProperties[$name] = DtoProperty::create($name, $data[$name], $types, $flags);
145
        }
146
147
        return $this->mappedProperties = $mappedProperties;
148
    }
149
150
    /**
151
     * Retrieve and cache the raw properties to map
152
     *
153
     * @return array
154
     * @throws InvalidDocCommentException
155
     */
156
    protected function cacheRawProperties(): array
157
    {
158
        if (isset($this->rawProperties)) {
159
            return $this->rawProperties;
160
        }
161
162
        if (false === $docComment = $this->reflection->getDocComment()) {
163
            throw new InvalidDocCommentException($this->dtoClass);
164
        }
165
166
        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

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

194
            $line = trim(fgets(/** @scrutinizer ignore-type */ $handle, 120));
Loading history...
195
            $begin = substr($line, 0, 3);
196
            $rawStatements .= $begin == 'use' ? $line : null;
197
        } while ($begin != '/**');
198
199
        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

199
        fclose(/** @scrutinizer ignore-type */ $handle);
Loading history...
200
201
        preg_match_all(static::RE_USE_STATEMENT, $rawStatements, $useMatches, PREG_SET_ORDER);
202
203
        foreach ($useMatches as $match) {
204
            $segments = explode('\\', $match[1]);
205
            $name = $match[2] ?? end($segments);
206
            $this->useStatements[$name] = $match[1];
207
        }
208
209
        return $this->useStatements;
210
    }
211
212
    /**
213
     * Parse the given raw property types
214
     *
215
     * @param string $rawTypes
216
     * @param array $useStatements
217
     * @return DtoPropertyTypes
218
     */
219
    protected function parseTypes(string $rawTypes, array $useStatements): DtoPropertyTypes
220
    {
221
        return array_reduce(explode('|', $rawTypes), function (DtoPropertyTypes $types, $rawType) use ($useStatements) {
222
            $name = str_replace('[]', '', $rawType, $count);
223
            $isCollection = $count > 0;
224
225
            // fully qualified class name exists
226
            if (strpos($rawType, '\\') === 0 && class_exists($name)) {
227
                return $types->addType(new DtoPropertyType(substr($name, 1), $isCollection));
228
            }
229
230
            if (isset($useStatements[$name])) {
231
                return $types->addType(new DtoPropertyType($useStatements[$name], $isCollection));
232
            }
233
234
            // class in DTO namespace exists
235
            if (class_exists($class = $this->reflection->getNamespaceName() . '\\' . $name)) {
236
                return $types->addType(new DtoPropertyType($class, $isCollection));
237
            }
238
239
            return $types->addType(new DtoPropertyType($name, $isCollection));
240
        }, new DtoPropertyTypes);
241
    }
242
}
243