AbstractClientRequest::verifyRequestData()   C
last analyzed

Complexity

Conditions 11
Paths 168

Size

Total Lines 40
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 1 Features 0
Metric Value
cc 11
eloc 25
c 4
b 1
f 0
nc 168
nop 0
dl 0
loc 40
rs 6.75

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
    // No double quotes (API clients would add backslashes)
14
    public const MSG_TPL_INVALID = 'Invalid data: \'%s\'.';
15
    public const MSG_TPL_MAXIMUM_LENGTH = 'Maximum length exceeded: \'%s\'. Limit: %s';
16
    public const MSG_TPL_REQUIRED = 'Missing required data: \'%s\'.';
17
18
    protected bool $allowMultipleDataObjects;
19
    protected RequestInterface $request;
20
    protected bool $processRequestData;
21
22
    /**
23
    * Request data.
24
    *
25
    * @var array<mixed>
26
    */
27
    protected array $requestData;
28
29
    public function __construct(RequestInterface $request)
30
    {
31
        $this->allowMultipleDataObjects = false;
32
        $this->request = $request;
33
        $requestMethod = $this->request->getMethod();
34
35
        if (\WebServCo\Framework\Http\Method::POST !== $requestMethod) {
36
            return;
37
        }
38
        $this->processRequestData = true;
39
        $requestBody = $this->request->getBody();
40
        if (!$requestBody) { // No problem if misisng, set to empty array.
41
            $this->requestData = [];
42
        } else {
43
            // If not missing, it needs to be valid.
44
            try {
45
                /**
46
                * @throws \JsonException
47
                */
48
                $requestData = \json_decode(
49
                    $requestBody,
50
                    true, // associative
51
                    512, // depth
52
                    \JSON_THROW_ON_ERROR, // flags
53
                );
54
                if (!\is_array($requestData)) {
55
                    throw new \InvalidArgumentException('Invalid root object.');
56
                }
57
                $this->requestData = $requestData;
58
            } catch (\Throwable $e) { // including \JsonException
59
                $this->throwInvalidException('root object');
60
            }
61
        }
62
    }
63
64
    protected function throwInvalidException(string $item): void
65
    {
66
        throw new ApiException(\sprintf(self::MSG_TPL_INVALID, $item));
67
    }
68
69
    protected function throwMaximumLengthException(string $item, int $maximumLength): void
70
    {
71
        throw new ApiException(\sprintf(self::MSG_TPL_MAXIMUM_LENGTH, $item, $maximumLength));
72
    }
73
74
    protected function throwRequiredException(string $item): void
75
    {
76
        throw new ApiException(\sprintf(self::MSG_TPL_REQUIRED, $item));
77
    }
78
79
    protected function verify(): bool
80
    {
81
        $this->verifyContentType();
82
        if ($this->processRequestData) {
83
            $this->verifyRequestData();
84
        }
85
        return true;
86
    }
87
88
    protected function verifyContentType(): bool
89
    {
90
        $contentType = $this->request->getContentType();
91
        $parts = \explode(';', (string) $contentType);
92
        if (Document::CONTENT_TYPE !== $parts[0]) {
93
            throw new \WebServCo\Framework\Exceptions\UnsupportedMediaTypeException(
94
                \sprintf('Unsupported request content type: %s.', (string) $contentType),
95
            );
96
        }
97
        return true;
98
    }
99
100
    protected function verifyRequestData(): bool
101
    {
102
        if (!\is_array($this->requestData)) {
0 ignored issues
show
introduced by
The condition is_array($this->requestData) is always true.
Loading history...
103
            $this->throwInvalidException('root object');
104
        }
105
        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...
106
            $this->throwRequiredException('root object');
107
        }
108
        foreach (['jsonapi', 'data'] as $item) {
109
            if (isset($this->requestData[$item])) {
110
                continue;
111
            }
112
113
            $this->throwRequiredException($item);
114
        }
115
        if (!isset($this->requestData['jsonapi']['version'])) {
116
            $this->throwRequiredException('jsonapi.version');
117
        }
118
        if (Document::VERSION !== $this->requestData['jsonapi']['version']) {
119
            throw new ApiException(
120
                \sprintf('Unsupported JSON API version: %s', $this->requestData['jsonapi']['version']),
121
            );
122
        }
123
        if (!\is_array($this->requestData['data'])) {
124
            $this->throwInvalidException('data');
125
        }
126
        $key = \key($this->requestData['data']);
127
        if (0 === $key) { //multiple data objects
128
            if (!$this->allowMultipleDataObjects) {
129
                throw new ApiException('Multiple data objects not allowed for this endpoint');
130
            }
131
            foreach ($this->requestData['data'] as $item) {
132
                $this->verifyData($item);
133
            }
134
        } else { // single data object
135
            $this->verifyData($this->requestData['data']);
136
        }
137
        $this->verifyMeta();
138
139
        return true;
140
    }
141
142
    /**
143
    * @param array<string,mixed> $data
144
    */
145
    protected function verifyData(array $data): bool
146
    {
147
        foreach (['type', 'attributes'] as $item) {
148
            if (isset($data[$item])) {
149
                continue;
150
            }
151
152
            $this->throwRequiredException(\sprintf('data.%s', $item));
153
        }
154
        if (empty($data['type'])) {
155
            $this->throwRequiredException('data.type');
156
        }
157
        if (!\is_array($data['attributes'])) {
158
            $this->throwInvalidException('data.attributes');
159
        }
160
161
        return true;
162
    }
163
164
    protected function verifyMeta(): bool
165
    {
166
        if (isset($this->requestData['meta'])) { // meta is optional
167
            if (!\is_array($this->requestData['meta'])) {
168
                $this->throwInvalidException('meta');
169
            }
170
        }
171
        return true;
172
    }
173
}
174