Passed
Push — 1.x ( d95dae...dd192d )
by Pavel
04:09 queued 01:48
created

BaseResource::resolveMappingType()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 34
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 18
c 0
b 0
f 0
dl 0
loc 34
rs 8.8333
cc 7
nc 8
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace DigitalCz\DigiSign\Resource;
6
7
use DateTime;
8
use DateTimeInterface;
9
use DigitalCz\DigiSign\Exception\RuntimeException;
10
use JsonSerializable;
11
use LogicException;
12
use Psr\Http\Message\ResponseInterface;
13
use ReflectionException;
14
use ReflectionProperty;
15
16
/**
17
 * Represents an API resource
18
 */
19
class BaseResource implements ResourceInterface
20
{
21
    /** @var array<string, array<string, string>> Cache of resolved mapping types */
22
    protected static $_mapping = []; // phpcs:ignore
23
24
    /** @var ResponseInterface Original API response */
25
    protected $_response; // phpcs:ignore
26
27
    /** @var mixed[] Original values from API response */
28
    protected $_result; // phpcs:ignore
29
30
    /**
31
     * @param mixed[] $result
32
     */
33
    public function __construct(array $result)
34
    {
35
        $this->_result = $result;
36
        $this->hydrate($result);
37
    }
38
39
    /** @inheritDoc */
40
    public function getResult(): array
41
    {
42
        return $this->_result;
43
    }
44
45
    /** @inheritDoc */
46
    public function toArray(): array
47
    {
48
        $values = get_object_vars($this);
49
50
        $result = [];
51
        foreach ($values as $property => $value) {
52
            // skip internal properties
53
            if (in_array($property, ['_mapping', '_response', '_result'], true)) {
54
                continue;
55
            }
56
57
            if ($value instanceof DateTimeInterface) {
58
                $value = $value->format(DateTimeInterface::ATOM);
59
            }
60
61
            if ($value instanceof JsonSerializable) {
62
                $value = $value->jsonSerialize();
63
            }
64
65
            if ($value instanceof Collection) {
66
                $value = array_map(static function (BaseResource $resource): array {
67
                    return $resource->toArray();
68
                }, $value->getArrayCopy());
69
            }
70
71
            $result[$property] = $value;
72
        }
73
74
        return $result;
75
    }
76
77
    public function self(): string
78
    {
79
        if (!isset($this->_result['_links']['self'])) {
80
            throw new RuntimeException('Resource has no self link');
81
        }
82
83
        return $this->_result['_links']['self'];
84
    }
85
86
    public function id(): string
87
    {
88
        if (!isset($this->_result['id'])) {
89
            throw new RuntimeException('Resource has no ID');
90
        }
91
92
        return $this->_result['id'];
93
    }
94
95
    /**
96
     * @return mixed[]
97
     */
98
    public function jsonSerialize(): array
99
    {
100
        return $this->toArray();
101
    }
102
103
    public function getResponse(): ResponseInterface
104
    {
105
        if (!isset($this->_response)) {
106
            throw new RuntimeException('Only resource returned from client has API response set');
107
        }
108
109
        return $this->_response;
110
    }
111
112
    public function setResponse(ResponseInterface $response): void
113
    {
114
        $this->_response = $response;
115
    }
116
117
    /**
118
     * @param mixed[] $values
119
     */
120
    protected function hydrate(array $values): void
121
    {
122
        foreach ($values as $property => $value) {
123
            $this->setProperty($property, $value);
124
        }
125
    }
126
127
    /**
128
     * @param mixed $value
129
     */
130
    protected function setProperty(string $property, $value): void
131
    {
132
        if ($value !== null) {
133
            $type = $this->getMappingType($property);
134
135
            // is Resource class
136
            if (is_a($type, self::class, true)) {
137
                $value = new $type($value);
138
            }
139
140
            // is Collection<Resource>
141
            if (is_array($value) && strpos($type, 'Collection') === 0) {
142
                // parse Resource class from type
143
                preg_match('/Collection<(.+)>/', $type, $matches);
144
                $resourceClass = $matches[1];
145
                $items = array_map(static function (array $itemValue) use ($resourceClass) {
146
                    return new $resourceClass($itemValue);
147
                }, $value);
148
                $value = new Collection($items);
149
            }
150
151
            if ($type === DateTime::class) {
152
                $value = new DateTime($value);
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type DigitalCz\DigiSign\Resource\Collection and object; however, parameter $datetime of DateTime::__construct() does only seem to accept string, 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

152
                $value = new DateTime(/** @scrutinizer ignore-type */ $value);
Loading history...
153
            }
154
        }
155
156
        $this->$property = $value; // @phpstan-ignore-line
157
    }
158
159
    protected function getMappingType(string $property): string
160
    {
161
        // cache resolved mapping types
162
        if (!isset(static::$_mapping[static::class][$property])) {
163
            static::$_mapping[static::class] = static::$_mapping[static::class] ?? [];
164
            static::$_mapping[static::class][$property] = $this->resolveMappingType($property);
165
        }
166
167
        return static::$_mapping[static::class][$property];
168
    }
169
170
    protected function resolveMappingType(string $property): string
171
    {
172
        try {
173
            $reflection = new ReflectionProperty($this, $property);
174
            $phpDoc = $reflection->getDocComment();
175
        } catch (ReflectionException $e) {
176
            return 'mixed'; // property may not exist
177
        }
178
179
        if ($phpDoc === false) {
180
            return 'mixed'; // no doc comment
181
        }
182
183
        if (preg_match('/@var\s+(?<type>[^\s]+)/', $phpDoc, $matches) !== 1) {
184
            return 'mixed'; // doc comment without @var type
185
        }
186
187
        $type = $matches['type'];
188
189
        if (class_exists($type)) {
190
            return $type; // type is FQCN
191
        }
192
193
        if (class_exists(__NAMESPACE__ . '\\' . $type)) {
194
            return __NAMESPACE__ . '\\' . $type; // type is class in same namespace
195
        }
196
197
        if (strpos($type, 'Collection') === 0) {
198
            $collectionType = $this->resolveCollectionMappingType($phpDoc);
199
200
            return "Collection<$collectionType>"; // type is collection
201
        }
202
203
        return $type;
204
    }
205
206
    protected function resolveCollectionMappingType(string $phpDoc): string
207
    {
208
        if (preg_match('/@var\s+Collection<(?<type>[^\s]+)>/', $phpDoc, $matches) !== 1) {
209
            throw new LogicException('Cannot resolve Collection type on ' . static::class . ' from ' . $phpDoc);
210
        }
211
212
        $type = $matches['type'];
213
214
        if (class_exists(__NAMESPACE__ . '\\' . $type)) {
215
            return __NAMESPACE__ . '\\' . $type; // type is class in same namespace
216
        }
217
218
        return $type;
219
    }
220
}
221