ci4
1<?php
2
3declare(strict_types=1);4
5/**
6* This file is part of CodeIgniter 4 framework.
7*
8* (c) CodeIgniter Foundation <admin@codeigniter.com>
9*
10* For the full copyright and license information, please view
11* the LICENSE file that was distributed with this source code.
12*/
13
14namespace CodeIgniter\API;15
16use CodeIgniter\Format\FormatterInterface;17use CodeIgniter\HTTP\IncomingRequest;18use CodeIgniter\HTTP\ResponseInterface;19
20/**
21* Provides common, more readable, methods to provide
22* consistent HTTP responses under a variety of common
23* situations when working as an API.
24*
25* @property bool $stringAsHtml Whether to treat string data as HTML in JSON response.
26* Setting `true` is only for backward compatibility.
27*/
28trait ResponseTrait29{
30/**31* Allows child classes to override the
32* status code that is used in their API.
33*
34* @var array<string, int>
35*/
36protected $codes = [37'created' => 201,38'deleted' => 200,39'updated' => 200,40'no_content' => 204,41'invalid_request' => 400,42'unsupported_response_type' => 400,43'invalid_scope' => 400,44'temporarily_unavailable' => 400,45'invalid_grant' => 400,46'invalid_credentials' => 400,47'invalid_refresh' => 400,48'no_data' => 400,49'invalid_data' => 400,50'access_denied' => 401,51'unauthorized' => 401,52'invalid_client' => 401,53'forbidden' => 403,54'resource_not_found' => 404,55'not_acceptable' => 406,56'resource_exists' => 409,57'conflict' => 409,58'resource_gone' => 410,59'payload_too_large' => 413,60'unsupported_media_type' => 415,61'too_many_requests' => 429,62'server_error' => 500,63'unsupported_grant_type' => 501,64'not_implemented' => 501,65];66
67/**68* How to format the response data.
69* Either 'json' or 'xml'. If null is set, it will be determined through
70* content negotiation.
71*
72* @var string|null
73* @phpstan-var 'html'|'json'|'xml'|null
74*/
75protected $format = 'json';76
77/**78* Current Formatter instance. This is usually set by ResponseTrait::format
79*
80* @var FormatterInterface|null
81*/
82protected $formatter;83
84/**85* Provides a single, simple method to return an API response, formatted
86* to match the requested format, with proper content-type and status code.
87*
88* @param array|string|null $data
89*
90* @return ResponseInterface
91*/
92protected function respond($data = null, ?int $status = null, string $message = '')93{94if ($data === null && $status === null) {95$status = 404;96$output = null;97$this->format($data);98} elseif ($data === null && is_numeric($status)) {99$output = null;100$this->format($data);101} else {102$status ??= 200;103$output = $this->format($data);104}105
106if ($output !== null) {107if ($this->format === 'json') {108return $this->response->setJSON($output)->setStatusCode($status, $message);109}110
111if ($this->format === 'xml') {112return $this->response->setXML($output)->setStatusCode($status, $message);113}114}115
116return $this->response->setBody($output)->setStatusCode($status, $message);117}118
119/**120* Used for generic failures that no custom methods exist for.
121*
122* @param array|string $messages
123* @param int $status HTTP status code
124* @param string|null $code Custom, API-specific, error code
125*
126* @return ResponseInterface
127*/
128protected function fail($messages, int $status = 400, ?string $code = null, string $customMessage = '')129{130if (! is_array($messages)) {131$messages = ['error' => $messages];132}133
134$response = [135'status' => $status,136'error' => $code ?? $status,137'messages' => $messages,138];139
140return $this->respond($response, $status, $customMessage);141}142
143// --------------------------------------------------------------------144// Response Helpers145// --------------------------------------------------------------------146
147/**148* Used after successfully creating a new resource.
149*
150* @param array|string|null $data
151*
152* @return ResponseInterface
153*/
154protected function respondCreated($data = null, string $message = '')155{156return $this->respond($data, $this->codes['created'], $message);157}158
159/**160* Used after a resource has been successfully deleted.
161*
162* @param array|string|null $data
163*
164* @return ResponseInterface
165*/
166protected function respondDeleted($data = null, string $message = '')167{168return $this->respond($data, $this->codes['deleted'], $message);169}170
171/**172* Used after a resource has been successfully updated.
173*
174* @param array|string|null $data
175*
176* @return ResponseInterface
177*/
178protected function respondUpdated($data = null, string $message = '')179{180return $this->respond($data, $this->codes['updated'], $message);181}182
183/**184* Used after a command has been successfully executed but there is no
185* meaningful reply to send back to the client.
186*
187* @return ResponseInterface
188*/
189protected function respondNoContent(string $message = 'No Content')190{191return $this->respond(null, $this->codes['no_content'], $message);192}193
194/**195* Used when the client is either didn't send authorization information,
196* or had bad authorization credentials. User is encouraged to try again
197* with the proper information.
198*
199* @return ResponseInterface
200*/
201protected function failUnauthorized(string $description = 'Unauthorized', ?string $code = null, string $message = '')202{203return $this->fail($description, $this->codes['unauthorized'], $code, $message);204}205
206/**207* Used when access is always denied to this resource and no amount
208* of trying again will help.
209*
210* @return ResponseInterface
211*/
212protected function failForbidden(string $description = 'Forbidden', ?string $code = null, string $message = '')213{214return $this->fail($description, $this->codes['forbidden'], $code, $message);215}216
217/**218* Used when a specified resource cannot be found.
219*
220* @return ResponseInterface
221*/
222protected function failNotFound(string $description = 'Not Found', ?string $code = null, string $message = '')223{224return $this->fail($description, $this->codes['resource_not_found'], $code, $message);225}226
227/**228* Used when the data provided by the client cannot be validated.
229*
230* @return ResponseInterface
231*
232* @deprecated Use failValidationErrors instead
233*/
234protected function failValidationError(string $description = 'Bad Request', ?string $code = null, string $message = '')235{236return $this->fail($description, $this->codes['invalid_data'], $code, $message);237}238
239/**240* Used when the data provided by the client cannot be validated on one or more fields.
241*
242* @param list<string>|string $errors
243*
244* @return ResponseInterface
245*/
246protected function failValidationErrors($errors, ?string $code = null, string $message = '')247{248return $this->fail($errors, $this->codes['invalid_data'], $code, $message);249}250
251/**252* Use when trying to create a new resource and it already exists.
253*
254* @return ResponseInterface
255*/
256protected function failResourceExists(string $description = 'Conflict', ?string $code = null, string $message = '')257{258return $this->fail($description, $this->codes['resource_exists'], $code, $message);259}260
261/**262* Use when a resource was previously deleted. This is different than
263* Not Found, because here we know the data previously existed, but is now gone,
264* where Not Found means we simply cannot find any information about it.
265*
266* @return ResponseInterface
267*/
268protected function failResourceGone(string $description = 'Gone', ?string $code = null, string $message = '')269{270return $this->fail($description, $this->codes['resource_gone'], $code, $message);271}272
273/**274* Used when the user has made too many requests for the resource recently.
275*
276* @return ResponseInterface
277*/
278protected function failTooManyRequests(string $description = 'Too Many Requests', ?string $code = null, string $message = '')279{280return $this->fail($description, $this->codes['too_many_requests'], $code, $message);281}282
283/**284* Used when there is a server error.
285*
286* @param string $description The error message to show the user.
287* @param string|null $code A custom, API-specific, error code.
288* @param string $message A custom "reason" message to return.
289*/
290protected function failServerError(string $description = 'Internal Server Error', ?string $code = null, string $message = ''): ResponseInterface291{292return $this->fail($description, $this->codes['server_error'], $code, $message);293}294
295// --------------------------------------------------------------------296// Utility Methods297// --------------------------------------------------------------------298
299/**300* Handles formatting a response. Currently, makes some heavy assumptions
301* and needs updating! :)
302*
303* @param array|string|null $data
304*
305* @return string|null
306*/
307protected function format($data = null)308{309$format = service('format');310
311$mime = ($this->format === null) ? $format->getConfig()->supportedResponseFormats[0]312: "application/{$this->format}";313
314// Determine correct response type through content negotiation if not explicitly declared315if (316! in_array($this->format, ['json', 'xml'], true)317&& $this->request instanceof IncomingRequest318) {319$mime = $this->request->negotiate(320'media',321$format->getConfig()->supportedResponseFormats,322false323);324}325
326$this->response->setContentType($mime);327
328// if we don't have a formatter, make one329if (! isset($this->formatter)) {330// if no formatter, use the default331$this->formatter = $format->getFormatter($mime);332}333
334$asHtml = $this->stringAsHtml ?? false;335
336// Returns as HTML.337if (338($mime === 'application/json' && $asHtml && is_string($data))339|| ($mime !== 'application/json' && is_string($data))340) {341// The content type should be text/... and not application/...342$contentType = $this->response->getHeaderLine('Content-Type');343$contentType = str_replace('application/json', 'text/html', $contentType);344$contentType = str_replace('application/', 'text/', $contentType);345$this->response->setContentType($contentType);346$this->format = 'html';347
348return $data;349}350
351if ($mime !== 'application/json') {352// Recursively convert objects into associative arrays353// Conversion not required for JSONFormatter354$data = json_decode(json_encode($data), true);355}356
357return $this->formatter->format($data);358}359
360/**361* Sets the format the response should be in.
362*
363* @param string|null $format Response format
364* @phpstan-param 'json'|'xml' $format
365*
366* @return $this
367*/
368protected function setResponseFormat(?string $format = null)369{370$this->format = ($format === null) ? null : strtolower($format);371
372return $this;373}374}
375