Issues (49)

src/Resource/BaseResource.php (1 issue)

Labels
Severity
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 Collection) {
62
                $value = $value->toArray();
63
            }
64
65
            if ($value instanceof JsonSerializable) {
66
                $value = $value->jsonSerialize();
67
            }
68
69
            $result[$property] = $value;
70
        }
71
72
        return $result;
73
    }
74
75
    public function self(): string
76
    {
77
        if (!isset($this->links()['self'])) {
78
            throw new RuntimeException('Resource has no self link');
79
        }
80
81
        return $this->links()['self'];
82
    }
83
84
    public function id(): string
85
    {
86
        if (!isset($this->_result['id'])) {
87
            throw new RuntimeException('Resource has no ID');
88
        }
89
90
        return $this->_result['id'];
91
    }
92
93
    /**
94
     * @return array<string, string>
95
     */
96
    public function links(): array
97
    {
98
        if (!isset($this->_result['_links']['self'])) {
99
            throw new RuntimeException('Resource has no links');
100
        }
101
102
        return $this->_result['_links'];
103
    }
104
105
    /**
106
     * @return mixed[]
107
     */
108
    public function jsonSerialize(): array
109
    {
110
        return $this->toArray();
111
    }
112
113
    public function getResponse(): ResponseInterface
114
    {
115
        if (!isset($this->_response)) {
116
            throw new RuntimeException('Only resource returned from client has API response set');
117
        }
118
119
        return $this->_response;
120
    }
121
122
    public function setResponse(ResponseInterface $response): void
123
    {
124
        $this->_response = $response;
125
    }
126
127
    /**
128
     * @param mixed[] $values
129
     */
130
    protected function hydrate(array $values): void
131
    {
132
        foreach ($values as $property => $value) {
133
            $this->setProperty($property, $value);
134
        }
135
    }
136
137
    /**
138
     * @param mixed $value
139
     */
140
    protected function setProperty(string $property, $value): void
141
    {
142
        if ($value !== null) {
143
            $type = $this->getMappingType($property);
144
145
            // is Resource class
146
            if (is_a($type, self::class, true)) {
147
                $value = new $type($value);
148
            }
149
150
            // is Collection<Resource>
151
            if (is_array($value) && strpos($type, 'Collection') === 0) {
152
                // parse Resource class from type
153
                preg_match('/Collection<(.+)>/', $type, $matches);
154
                $resourceClass = $matches[1];
155
                $value = new Collection($value, $resourceClass);
156
            }
157
158
            if ($type === DateTime::class) {
159
                $value = new DateTime($value);
0 ignored issues
show
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

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