Passed
Push — master ( 7dd677...d26f0a )
by Radu
07:58
created

src/WebServCo/Api/AbstractClientRequest.php (1 issue)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace WebServCo\Api;
6
7
use WebServCo\Api\Exceptions\ApiException;
8
use WebServCo\Api\JsonApi\Document;
9
use WebServCo\Framework\Interfaces\RequestInterface;
10
11
abstract class AbstractClientRequest
12
{
13
    public const MSG_TPL_INVALID = 'Invalid data: %s';
14
    public const MSG_TPL_MAXIMUM_LENGTH = 'Maximum length exceeded: %s: %s';
15
    public const MSG_TPL_REQUIRED = 'Missing required data: %s';
16
17
    protected bool $allowMultipleDataObjects;
18
    protected RequestInterface $request;
19
    protected bool $processRequestData;
20
21
    /**
22
    * Request data.
23
    *
24
    * @var array<mixed>
25
    */
26
    protected array $requestData;
27
28
    public function __construct(RequestInterface $request)
29
    {
30
        $this->allowMultipleDataObjects = false;
31
        $this->request = $request;
32
        $requestMethod = $this->request->getMethod();
33
34
        if (\WebServCo\Framework\Http\Method::POST !== $requestMethod) {
35
            return;
36
        }
37
        $this->processRequestData = true;
38
        try {
39
            // @throws \JsonException
40
            $this->requestData = \json_decode(
41
                $this->request->getBody(),
42
                true, // associative
43
                512, // depth
44
                \JSON_THROW_ON_ERROR, // flags
45
            )
46
            ?? [];
47
        } catch (\JsonException $e) {
48
            $this->throwInvalidException('root object');
49
        }
50
    }
51
52
    protected function throwInvalidException(string $item): void
53
    {
54
        throw new ApiException(\sprintf(self::MSG_TPL_INVALID, $item));
55
    }
56
57
    protected function throwMaximumLengthException(string $item, int $maximumLength): void
58
    {
59
        throw new ApiException(\sprintf(self::MSG_TPL_MAXIMUM_LENGTH, $item, $maximumLength));
60
    }
61
62
    protected function throwRequiredException(string $item): void
63
    {
64
        throw new ApiException(\sprintf(self::MSG_TPL_REQUIRED, $item));
65
    }
66
67
    protected function verify(): bool
68
    {
69
        $this->verifyContentType();
70
        if ($this->processRequestData) {
71
            $this->verifyRequestData();
72
        }
73
        return true;
74
    }
75
76
    protected function verifyContentType(): bool
77
    {
78
        $contentType = $this->request->getContentType();
79
        $parts = \explode(';', (string) $contentType);
80
        if (Document::CONTENT_TYPE !== $parts[0]) {
81
            throw new \WebServCo\Framework\Exceptions\UnsupportedMediaTypeException(
82
                \sprintf('Unsupported request content type: %s.', (string) $contentType),
83
            );
84
        }
85
        return true;
86
    }
87
88
    protected function verifyRequestData(): bool
89
    {
90
        if (!\is_array($this->requestData)) {
91
            $this->throwInvalidException('root object');
92
        }
93
        if (!$this->requestData) { // check if empty array, could also mean the json vas invalid
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->requestData 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...
94
            $this->throwRequiredException('root object');
95
        }
96
        foreach (['jsonapi', 'data'] as $item) {
97
            if (isset($this->requestData[$item])) {
98
                continue;
99
            }
100
101
            $this->throwRequiredException($item);
102
        }
103
        if (!isset($this->requestData['jsonapi']['version'])) {
104
            $this->throwRequiredException('jsonapi.version');
105
        }
106
        if (Document::VERSION !== $this->requestData['jsonapi']['version']) {
107
            throw new ApiException(
108
                \sprintf('Unsupported JSON API version: %s', $this->requestData['jsonapi']['version']),
109
            );
110
        }
111
        if (!\is_array($this->requestData['data'])) {
112
            $this->throwInvalidException('data');
113
        }
114
        $key = \key($this->requestData['data']);
115
        if (0 === $key) { //multiple data objects
116
            if (!$this->allowMultipleDataObjects) {
117
                throw new ApiException('Multiple data objects not allowed for this endpoint');
118
            }
119
            foreach ($this->requestData['data'] as $item) {
120
                $this->verifyData($item);
121
            }
122
        } else { // single data object
123
            $this->verifyData($this->requestData['data']);
124
        }
125
        $this->verifyMeta();
126
127
        return true;
128
    }
129
130
    /**
131
    * @param array<string,mixed> $data
132
    */
133
    protected function verifyData(array $data): bool
134
    {
135
        foreach (['type', 'attributes'] as $item) {
136
            if (isset($data[$item])) {
137
                continue;
138
            }
139
140
            $this->throwRequiredException(\sprintf('data.%s', $item));
141
        }
142
        if (empty($data['type'])) {
143
            $this->throwRequiredException('data.type');
144
        }
145
        if (!\is_array($data['attributes'])) {
146
            $this->throwInvalidException('data.attributes');
147
        }
148
149
        return true;
150
    }
151
152
    protected function verifyMeta(): bool
153
    {
154
        if (isset($this->requestData['meta'])) { // meta is optional
155
            if (!\is_array($this->requestData['meta'])) {
156
                $this->throwInvalidException('meta');
157
            }
158
        }
159
        return true;
160
    }
161
}
162