Passed
Branch feature/first-implementation (30573d)
by Andrea Marco
02:38
created

DtoPropertiesMapper   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 226
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 70
c 0
b 0
f 0
dl 0
loc 226
ccs 64
cts 64
cp 1
rs 10
wmc 25

7 Methods

Rating   Name   Duplication   Size   Complexity  
A reflectDto() 0 6 2
A for() 0 3 1
A __construct() 0 4 1
A cacheUseStatements() 0 27 5
A map() 0 24 6
A parseTypes() 0 22 5
A cacheRawProperties() 0 20 5
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 30
    protected function __construct(string $dtoClass)
83
    {
84 30
        $this->dtoClass = $dtoClass;
85 30
        $this->reflection = $this->reflectDto();
86 27
    }
87
88
    /**
89
     * Retrieve the reflection of the given DTO class
90
     *
91
     * @return ReflectionClass
92
     * @throws DtoNotFoundException
93
     */
94 30
    protected function reflectDto(): ReflectionClass
95
    {
96
        try {
97 30
            return new ReflectionClass($this->dtoClass);
98 3
        } catch (ReflectionException $e) {
99 3
            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 174
    public static function for(string $dtoClass): DtoPropertiesMapper
111
    {
112 174
        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 168
    public function map(array $data, int $flags): array
125
    {
126 168
        $mappedProperties = [];
127 168
        $rawProperties = $this->cacheRawProperties();
128 165
        $useStatements = $this->cacheUseStatements();
129
130 165
        foreach ($rawProperties as $name => $rawTypes) {
131 138
            $cachedProperty = $this->mappedProperties[$name] ?? null;
132 138
            $types = $cachedProperty ? $cachedProperty->getTypes() : $this->parseTypes($rawTypes, $useStatements);
133
134 138
            if (!array_key_exists($name, $data)) {
135 135
                if ($types->haveDefaultValue($flags)) {
136 9
                    $data[$name] = $types->getDefaultValue($flags);
137 129
                } elseif ($flags & PARTIAL) {
138 126
                    continue;
139
                } else {
140 3
                    throw new MissingValueException($this->dtoClass, $name);
141
                }
142
            }
143
144 129
            $mappedProperties[$name] = DtoProperty::create($name, $data[$name], $types, $flags);
145
        }
146
147 162
        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 168
    protected function cacheRawProperties(): array
157
    {
158 168
        if (isset($this->rawProperties)) {
159 147
            return $this->rawProperties;
160
        }
161
162 24
        if (false === $docComment = $this->reflection->getDocComment()) {
163 3
            throw new InvalidDocCommentException($this->dtoClass);
164
        }
165
166 21
        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 9
            return $this->rawProperties = [];
168
        }
169
170 18
        foreach ($matches as $match) {
171 18
            [, $rawTypes, $name] = $match;
172 18
            $this->rawProperties[$name] = $rawTypes;
173
        }
174
175 18
        return $this->rawProperties;
176
    }
177
178
    /**
179
     * Retrieve and cache the DTO "use" statements
180
     *
181
     * @return array
182
     */
183 165
    protected function cacheUseStatements(): array
184
    {
185 165
        if (isset($this->useStatements)) {
186 147
            return $this->useStatements;
187
        }
188
189 21
        $this->useStatements = [];
190 21
        $rawStatements = null;
191 21
        $handle = fopen($this->reflection->getFileName(), 'rb');
192
193
        do {
194 21
            $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 21
            $begin = substr($line, 0, 3);
196 21
            $rawStatements .= $begin == 'use' ? $line : null;
197 21
        } while ($begin != '/**');
198
199 21
        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 21
        preg_match_all(static::RE_USE_STATEMENT, $rawStatements, $useMatches, PREG_SET_ORDER);
202
203 21
        foreach ($useMatches as $match) {
204 21
            $segments = explode('\\', $match[1]);
205 21
            $name = $match[2] ?? end($segments);
206 21
            $this->useStatements[$name] = $match[1];
207
        }
208
209 21
        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 138
    protected function parseTypes(string $rawTypes, array $useStatements): DtoPropertyTypes
220
    {
221
        return array_reduce(explode('|', $rawTypes), function (DtoPropertyTypes $types, $rawType) use ($useStatements) {
222 138
            $name = str_replace('[]', '', $rawType, $count);
223 138
            $isCollection = $count > 0;
224
225
            // fully qualified class name exists
226 138
            if (strpos($rawType, '\\') === 0 && class_exists($name)) {
227 36
                return $types->addType(new DtoPropertyType(substr($name, 1), $isCollection));
228
            }
229
230 138
            if (isset($useStatements[$name])) {
231 33
                return $types->addType(new DtoPropertyType($useStatements[$name], $isCollection));
232
            }
233
234
            // class in DTO namespace exists
235 138
            if (class_exists($class = $this->reflection->getNamespaceName() . '\\' . $name)) {
236 126
                return $types->addType(new DtoPropertyType($class, $isCollection));
237
            }
238
239 138
            return $types->addType(new DtoPropertyType($name, $isCollection));
240 138
        }, new DtoPropertyTypes);
241
    }
242
}
243