1 | <?php |
||
2 | |||
3 | namespace kalanis\Restful\Application; |
||
4 | |||
5 | |||
6 | use kalanis\Restful\Exceptions\InvalidArgumentException; |
||
7 | use kalanis\Restful\Exceptions\InvalidStateException; |
||
8 | use kalanis\Restful\IResource; |
||
9 | use kalanis\Restful\Mapping\MapperContext; |
||
10 | use Nette\Http\IRequest; |
||
11 | use Nette\Http\IResponse; |
||
12 | use function str_contains; |
||
13 | |||
14 | |||
15 | /** |
||
16 | * REST ResponseFactory |
||
17 | * @package kalanis\Restful\Application |
||
18 | */ |
||
19 | 1 | class ResponseFactory implements IResponseFactory |
|
20 | { |
||
21 | |||
22 | /** @var string|null JSONP request key */ |
||
23 | private ?string $jsonp = null; |
||
24 | |||
25 | /** @var string pretty print key */ |
||
26 | private string $prettyPrintKey = 'prettyPrint'; |
||
27 | |||
28 | private bool $prettyPrint = true; |
||
29 | |||
30 | /** @var array<string, class-string> */ |
||
0 ignored issues
–
show
Documentation
Bug
introduced
by
![]() |
|||
31 | private array $responses = [ |
||
32 | IResource::JSON => Responses\TextResponse::class, |
||
33 | IResource::JSONP => Responses\JsonpResponse::class, |
||
34 | IResource::QUERY => Responses\TextResponse::class, |
||
35 | IResource::XML => Responses\TextResponse::class, |
||
36 | IResource::FILE => Responses\FileResponse::class, |
||
37 | IResource::NULL => Responses\NullResponse::class |
||
38 | ]; |
||
39 | |||
40 | 1 | public function __construct( |
|
41 | private IResponse $response, |
||
42 | private readonly IRequest $request, |
||
43 | private readonly MapperContext $mapperContext, |
||
44 | ) |
||
45 | { |
||
46 | 1 | } |
|
47 | |||
48 | /** |
||
49 | * Get JSONP key |
||
50 | * @return string|null [type] [description] |
||
51 | */ |
||
52 | public function getJsonp(): ?string |
||
53 | { |
||
54 | return $this->jsonp; |
||
55 | } |
||
56 | |||
57 | /** |
||
58 | * Set JSONP key |
||
59 | */ |
||
60 | public function setJsonp(?string $jsonp): self |
||
61 | { |
||
62 | 1 | $this->jsonp = $jsonp; |
|
63 | 1 | return $this; |
|
64 | } |
||
65 | |||
66 | /** |
||
67 | * Set pretty print key |
||
68 | */ |
||
69 | public function setPrettyPrintKey(string $prettyPrintKey): self |
||
70 | { |
||
71 | 1 | $this->prettyPrintKey = $prettyPrintKey; |
|
72 | 1 | return $this; |
|
73 | } |
||
74 | |||
75 | /** |
||
76 | * Register new response type to factory |
||
77 | * @param string $mimeType |
||
78 | * @param class-string $responseClass |
||
0 ignored issues
–
show
|
|||
79 | * @throws InvalidArgumentException |
||
80 | * @return $this |
||
81 | * |
||
82 | */ |
||
83 | public function registerResponse(string $mimeType, string $responseClass): static |
||
84 | { |
||
85 | 1 | if (!class_exists($responseClass)) { |
|
86 | 1 | throw new InvalidArgumentException('Response class does not exist.'); |
|
87 | } |
||
88 | |||
89 | 1 | $this->responses[$mimeType] = $responseClass; |
|
90 | 1 | return $this; |
|
91 | } |
||
92 | |||
93 | /** |
||
94 | * Unregister API response from factory |
||
95 | */ |
||
96 | public function unregisterResponse(string $mimeType): void |
||
97 | { |
||
98 | unset($this->responses[$mimeType]); |
||
99 | } |
||
100 | |||
101 | /** |
||
102 | * Set HTTP response |
||
103 | */ |
||
104 | public function setHttpResponse(IResponse $response): self |
||
105 | { |
||
106 | $this->response = $response; |
||
107 | return $this; |
||
108 | } |
||
109 | |||
110 | /** |
||
111 | * Create new api response |
||
112 | * @param IResource $resource |
||
113 | * @param string|null $contentType |
||
114 | * @throws InvalidStateException |
||
115 | * @return Responses\IResponse |
||
116 | */ |
||
117 | public function create(IResource $resource, ?string $contentType = null): Responses\IResponse |
||
118 | { |
||
119 | 1 | if (is_null($contentType)) { |
|
120 | 1 | $contentType = is_null($this->jsonp) || empty($this->request->getQuery($this->jsonp)) |
|
121 | 1 | ? $this->getPreferredContentType(strval($this->request->getHeader('Accept'))) |
|
122 | 1 | : IResource::JSONP; |
|
123 | } |
||
124 | |||
125 | 1 | if (!isset($this->responses[$contentType])) { |
|
126 | throw new InvalidStateException('Unregistered API response for ' . $contentType); |
||
127 | } |
||
128 | |||
129 | 1 | if (!class_exists($this->responses[$contentType])) { |
|
130 | throw new InvalidStateException('API response class does not exist.'); |
||
131 | } |
||
132 | |||
133 | 1 | if (empty($resource->getData())) { |
|
134 | 1 | $this->response->setCode(204); // No content |
|
135 | 1 | $reflection = new \ReflectionClass($this->responses[$contentType]); |
|
136 | 1 | $response = $reflection->newInstance( |
|
137 | 1 | $resource->getData(), |
|
138 | 1 | $this->mapperContext->getMapper($contentType), |
|
139 | ); |
||
140 | /** @var Responses\IResponse $response */ |
||
141 | 1 | return $response; |
|
142 | } |
||
143 | |||
144 | 1 | $responseClass = $this->responses[$contentType]; |
|
145 | 1 | $reflection = new \ReflectionClass($responseClass); |
|
146 | 1 | $response = $reflection->newInstance( |
|
147 | 1 | $resource->getData(), |
|
148 | 1 | $this->mapperContext->getMapper($contentType), |
|
149 | $contentType, |
||
150 | ); |
||
151 | /** @var Responses\IResponse $response */ |
||
152 | 1 | if ($response instanceof Responses\BaseResponse) { |
|
153 | 1 | $response->setPrettyPrint($this->isPrettyPrint()); |
|
154 | } |
||
155 | 1 | return $response; |
|
156 | } |
||
157 | |||
158 | /** |
||
159 | * Get preferred request content type |
||
160 | * @param string $contentType may be separated with comma |
||
161 | * @throws InvalidStateException If Accept header is unknown |
||
162 | * @return string |
||
163 | */ |
||
164 | protected function getPreferredContentType(string $contentType): string |
||
165 | { |
||
166 | 1 | $accept = explode(',', $contentType); |
|
167 | 1 | $acceptableTypes = array_keys($this->responses); |
|
168 | 1 | if (!$contentType) { |
|
169 | return $acceptableTypes[0]; |
||
170 | } |
||
171 | 1 | foreach ($accept as $mimeType) { |
|
172 | 1 | if ('*/*' === $mimeType) return $acceptableTypes[0]; |
|
173 | 1 | foreach ($acceptableTypes as $formatMime) { |
|
174 | 1 | if (empty($formatMime)) { |
|
175 | 1 | continue; |
|
176 | } |
||
177 | 1 | if (str_contains($mimeType, $formatMime)) { |
|
178 | 1 | return $formatMime; |
|
179 | } |
||
180 | } |
||
181 | } |
||
182 | 1 | throw new InvalidStateException('Unknown Accept header: ' . $contentType); |
|
183 | } |
||
184 | |||
185 | /** |
||
186 | * Is pretty print enabled |
||
187 | * @return boolean |
||
188 | */ |
||
189 | protected function isPrettyPrint(): bool |
||
190 | { |
||
191 | 1 | $prettyPrintKey = $this->request->getQuery($this->prettyPrintKey); |
|
192 | 1 | if ('false' === $prettyPrintKey) { |
|
193 | return false; |
||
194 | } |
||
195 | 1 | if ('true' === $prettyPrintKey) { |
|
196 | return true; |
||
197 | } |
||
198 | 1 | return $this->prettyPrint; |
|
199 | } |
||
200 | |||
201 | /** |
||
202 | * Set pretty print |
||
203 | */ |
||
204 | public function setPrettyPrint(bool $prettyPrint): self |
||
205 | { |
||
206 | $this->prettyPrint = $prettyPrint; |
||
207 | return $this; |
||
208 | } |
||
209 | |||
210 | /** |
||
211 | * Is given content type acceptable for response |
||
212 | */ |
||
213 | public function isAcceptable(string $contentType): bool |
||
214 | { |
||
215 | try { |
||
216 | 1 | $this->getPreferredContentType($contentType); |
|
217 | 1 | return true; |
|
218 | 1 | } catch (InvalidStateException) { |
|
219 | 1 | return false; |
|
220 | } |
||
221 | } |
||
222 | } |
||
223 |