Skip to content

Commit 5095340

Browse files
committed
Nearly direct copies from api-tools
1 parent f2b80ed commit 5095340

File tree

4 files changed

+414
-0
lines changed

4 files changed

+414
-0
lines changed

src/ApiProblem.php

+363
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ApiSkeletons\Laravel\ApiProblem;
6+
7+
use Exception;
8+
use ApiSkeletons\Laravel\ApiProblem\Exception\InvalidArgumentException;
9+
use ApiSkeletons\Laravel\ApiProblem\Exception\ProblemExceptionInterface;
10+
use Throwable;
11+
12+
use function array_key_exists;
13+
use function array_keys;
14+
use function array_merge;
15+
use function count;
16+
use function get_class;
17+
use function in_array;
18+
use function is_numeric;
19+
use function sprintf;
20+
use function strtolower;
21+
use function trim;
22+
23+
/**
24+
* Object describing an API-Problem payload.
25+
*/
26+
class ApiProblem
27+
{
28+
/**
29+
* Content type for api problem response
30+
*/
31+
public const CONTENT_TYPE = 'application/problem+json';
32+
33+
/**
34+
* Additional details to include in report.
35+
*
36+
* @var array
37+
*/
38+
protected $additionalDetails = [];
39+
40+
/**
41+
* URL describing the problem type; defaults to HTTP status codes.
42+
*
43+
* @var string
44+
*/
45+
protected $type = 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html';
46+
47+
/**
48+
* Description of the specific problem.
49+
*
50+
* @var string|Exception|Throwable
51+
*/
52+
protected $detail = '';
53+
54+
/**
55+
* Whether or not to include a stack trace and previous
56+
* exceptions when an exception is provided for the detail.
57+
*
58+
* @var bool
59+
*/
60+
protected $detailIncludesStackTrace = false;
61+
62+
/**
63+
* HTTP status for the error.
64+
*
65+
* @var int
66+
*/
67+
protected $status;
68+
69+
/**
70+
* Normalized property names for overloading.
71+
*
72+
* @var array
73+
*/
74+
protected $normalizedProperties = [
75+
'type' => 'type',
76+
'status' => 'status',
77+
'title' => 'title',
78+
'detail' => 'detail',
79+
];
80+
81+
/**
82+
* Status titles for common problems.
83+
*
84+
* @var array
85+
*/
86+
protected $problemStatusTitles = [
87+
// CLIENT ERROR
88+
400 => 'Bad Request',
89+
401 => 'Unauthorized',
90+
402 => 'Payment Required',
91+
403 => 'Forbidden',
92+
404 => 'Not Found',
93+
405 => 'Method Not Allowed',
94+
406 => 'Not Acceptable',
95+
407 => 'Proxy Authentication Required',
96+
408 => 'Request Time-out',
97+
409 => 'Conflict',
98+
410 => 'Gone',
99+
411 => 'Length Required',
100+
412 => 'Precondition Failed',
101+
413 => 'Request Entity Too Large',
102+
414 => 'Request-URI Too Large',
103+
415 => 'Unsupported Media Type',
104+
416 => 'Requested range not satisfiable',
105+
417 => 'Expectation Failed',
106+
418 => 'I\'m a teapot',
107+
422 => 'Unprocessable Entity',
108+
423 => 'Locked',
109+
424 => 'Failed Dependency',
110+
425 => 'Unordered Collection',
111+
426 => 'Upgrade Required',
112+
428 => 'Precondition Required',
113+
429 => 'Too Many Requests',
114+
431 => 'Request Header Fields Too Large',
115+
// SERVER ERROR
116+
500 => 'Internal Server Error',
117+
501 => 'Not Implemented',
118+
502 => 'Bad Gateway',
119+
503 => 'Service Unavailable',
120+
504 => 'Gateway Time-out',
121+
505 => 'HTTP Version not supported',
122+
506 => 'Variant Also Negotiates',
123+
507 => 'Insufficient Storage',
124+
508 => 'Loop Detected',
125+
511 => 'Network Authentication Required',
126+
];
127+
128+
/**
129+
* Title of the error.
130+
*
131+
* @var string
132+
*/
133+
protected $title;
134+
135+
/**
136+
* Create an instance using the provided information. If nothing is
137+
* provided for the type field, the class default will be used;
138+
* if the status matches any known, the title field will be selected
139+
* from $problemStatusTitles as a result.
140+
*
141+
* @param int|string $status
142+
* @param string|Exception|Throwable $detail
143+
* @param string $type
144+
* @param string $title
145+
* @param array $additional
146+
*/
147+
public function __construct($status, $detail, $type = null, $title = null, array $additional = [])
148+
{
149+
if ($detail instanceof ProblemExceptionInterface) {
150+
if (null === $type) {
151+
$type = $detail->getType();
152+
}
153+
if (null === $title) {
154+
$title = $detail->getTitle();
155+
}
156+
if (empty($additional)) {
157+
$additional = $detail->getAdditionalDetails();
158+
}
159+
}
160+
161+
// Ensure a valid HTTP status
162+
if (
163+
! is_numeric($status)
164+
|| ($status < 100)
165+
|| ($status > 599)
166+
) {
167+
$status = 500;
168+
}
169+
170+
$this->status = (int) $status;
171+
$this->detail = $detail;
172+
$this->title = $title;
173+
174+
if (null !== $type) {
175+
$this->type = $type;
176+
}
177+
178+
$this->additionalDetails = $additional;
179+
}
180+
181+
/**
182+
* Retrieve properties.
183+
*
184+
* @param string $name
185+
* @return mixed
186+
* @throws InvalidArgumentException
187+
*/
188+
public function __get($name)
189+
{
190+
$normalized = strtolower($name);
191+
if (in_array($normalized, array_keys($this->normalizedProperties))) {
192+
$prop = $this->normalizedProperties[$normalized];
193+
194+
return $this->{$prop};
195+
}
196+
197+
if (isset($this->additionalDetails[$name])) {
198+
return $this->additionalDetails[$name];
199+
}
200+
201+
if (isset($this->additionalDetails[$normalized])) {
202+
return $this->additionalDetails[$normalized];
203+
}
204+
205+
throw new InvalidArgumentException(sprintf(
206+
'Invalid property name "%s"',
207+
$name
208+
));
209+
}
210+
211+
/**
212+
* Cast to an array.
213+
*
214+
* @return array
215+
*/
216+
public function toArray()
217+
{
218+
$problem = [
219+
'type' => $this->type,
220+
'title' => $this->getTitle(),
221+
'status' => $this->getStatus(),
222+
'detail' => $this->getDetail(),
223+
];
224+
// Required fields should always overwrite additional fields
225+
return array_merge($this->additionalDetails, $problem);
226+
}
227+
228+
/**
229+
* Set the flag indicating whether an exception detail should include a
230+
* stack trace and previous exception information.
231+
*
232+
* @param bool $flag
233+
* @return ApiProblem
234+
*/
235+
public function setDetailIncludesStackTrace($flag)
236+
{
237+
$this->detailIncludesStackTrace = (bool) $flag;
238+
239+
return $this;
240+
}
241+
242+
/**
243+
* Retrieve the API-Problem detail.
244+
*
245+
* If an exception was provided, creates the detail message from it;
246+
* otherwise, detail as provided is used.
247+
*
248+
* @return string
249+
*/
250+
protected function getDetail()
251+
{
252+
if ($this->detail instanceof Throwable || $this->detail instanceof Exception) {
253+
return $this->createDetailFromException();
254+
}
255+
256+
return $this->detail;
257+
}
258+
259+
/**
260+
* Retrieve the API-Problem HTTP status code.
261+
*
262+
* If an exception was provided, creates the status code from it;
263+
* otherwise, code as provided is used.
264+
*/
265+
protected function getStatus(): int
266+
{
267+
if ($this->detail instanceof Throwable || $this->detail instanceof Exception) {
268+
$this->status = (int) $this->createStatusFromException();
269+
}
270+
271+
return $this->status;
272+
}
273+
274+
/**
275+
* Retrieve the title.
276+
*
277+
* If the default $type is used, and the $status is found in
278+
* $problemStatusTitles, then use the matching title.
279+
*
280+
* If no title was provided, and the above conditions are not met, use the
281+
* string 'Unknown'.
282+
*
283+
* Otherwise, use the title provided.
284+
*
285+
* @return string
286+
*/
287+
protected function getTitle()
288+
{
289+
if (null !== $this->title) {
290+
return $this->title;
291+
}
292+
293+
if (
294+
null === $this->title
295+
&& $this->type === 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html'
296+
&& array_key_exists($this->getStatus(), $this->problemStatusTitles)
297+
) {
298+
return $this->problemStatusTitles[$this->status];
299+
}
300+
301+
if ($this->detail instanceof Throwable) {
302+
return get_class($this->detail);
303+
}
304+
305+
if (null === $this->title) {
306+
return 'Unknown';
307+
}
308+
309+
return $this->title;
310+
}
311+
312+
/**
313+
* Create detail message from an exception.
314+
*
315+
* @return string
316+
*/
317+
protected function createDetailFromException()
318+
{
319+
/** @var Exception|Throwable $e */
320+
$e = $this->detail;
321+
322+
if (! $this->detailIncludesStackTrace) {
323+
return $e->getMessage();
324+
}
325+
326+
$message = trim($e->getMessage());
327+
$this->additionalDetails['trace'] = $e->getTrace();
328+
329+
$previous = [];
330+
$e = $e->getPrevious();
331+
while ($e) {
332+
$previous[] = [
333+
'code' => (int) $e->getCode(),
334+
'message' => trim($e->getMessage()),
335+
'trace' => $e->getTrace(),
336+
];
337+
$e = $e->getPrevious();
338+
}
339+
if (count($previous)) {
340+
$this->additionalDetails['exception_stack'] = $previous;
341+
}
342+
343+
return $message;
344+
}
345+
346+
/**
347+
* Create HTTP status from an exception.
348+
*
349+
* @return int|string
350+
*/
351+
protected function createStatusFromException()
352+
{
353+
/** @var Exception|Throwable $e */
354+
$e = $this->detail;
355+
$status = $e->getCode();
356+
357+
if ($status) {
358+
return $status;
359+
}
360+
361+
return 500;
362+
}
363+
}

src/Exception/ExceptionInterface.php

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ApiSkeletons\Laravel\ApiProblem\Exception;
6+
7+
/**
8+
* Marker interface; catch this to catch any module-specific exception.
9+
*/
10+
interface ExceptionInterface
11+
{
12+
}
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ApiSkeletons\Laravel\ApiProblem\Exception;
6+
7+
use InvalidArgumentException as PHPInvalidArgumentException;
8+
9+
class InvalidArgumentException extends PHPInvalidArgumentException implements ExceptionInterface
10+
{
11+
}

0 commit comments

Comments
 (0)