𑅃 API Problem (RFC 9457) objects that can be used as application/problem+json documents.
composer require vexed/vexeduse Vexed\Problem;
$problem = new Problem(
type: 'https://example.com/faq/order-quantity-minimums',
title: 'Minimum Quantity',
detail: 'The minimum order quantity for this product is 100 units.',
status: 400,
);
// They can be modified in place...
$problem->instance = '/cart/checkout';
// They can have extensions...
$problem->extend('refcode', 'MPQ.100');
// They can be converted to arrays...
$arr = $problem->toArray();
// They can be encoded directly to JSON...
$json = json_encode($problem);Any of the standard RFC properties can be set:
typea URI reference identifying the problem typetitlea short, human-readable summary of the problem typedetaila human-readable explanation specific to this occurrence of the problemstatusthe HTTP status codeinstancea URI reference that identifies the specific occurrence of the problem
Note that these names are reserved and CANNOT be used as extension names. Attempting to do so will cause
a ProblemException to be thrown.
There are 40 HTTP problem classes available in Vexed\Http\Client and Vexed\Http\Server, such as:
Vexed\Http\Client\BadRequestVexed\Http\Client\NotFoundVexed\Http\Server\InternalServerErrorVexed\Http\Server\NotImplemented- ... and 36 other classes.
Each of the HTTP problems have an immutable status and title with the RFC 9110 reason phrase.
The Vexed\ExceptionTransformer converts any Throwable into a Problem:
use Throwable;
use Vexed\ExceptionTransformer;
$transformer = new ExceptionTransformer();
try {
return $this->service->operation();
} catch (Throwable $e) {
$problem = $transformer->transform($e);
}By default, transform() will return an InternalServerError. This can be changed by defining a map that
associates a throwable class/interface with a factory that creates a problem. The transformer checks the throwable
class first, then its parent classes from the most immediate upward, then its interfaces. The first match wins.
When nothing matches, InternalServerError is returned:
use Vexed\ExceptionTransformer;
use Vexed\Http\Client\BadRequest;
use Vexed\Http\Client\NotFound;
$transformer = new ExceptionTransformer(
map: [
NotFoundException::class => fn() => new NotFound(),
ValidationException::class => fn(ValidationException $e) => new BadRequest()->extend('errors', $e->getErrors()),
],
classExtension: 'exception',
messageExtension: 'message',
);
try {
$order = $this->orders->find($id);
$order->prepare();
} catch (Throwable $e) {
$problem = $transformer->transform($e);
}The transformer can also add some extensions to the problem:
- When
classExtensionis defined, it will add$throwable::classas an extension. - When
messageExtensionis defined, it will add$throwable->getMessage()as an extension.
Both are entirely optional and disabled by default.
use Throwable;
$transformer = new ExceptionTransformer(
classExtension: 'exception',
messageExtension: 'message',
);
try {
return $this->orders->process($order);
} catch (Throwable $e) {
// The problem has been extended with `{exception: string, message: string}`
$problem = $transformer->transform($e);
}This project uses Mago for lint, formatting, and static analysis.
composer run fix # automatically fix lint, analysis, and formatting issues
composer run format # format source code
composer run check # check style
composer run lint # lint code
composer run analyze # static analysis
composer run test # unit testing with 100% coverage enforced
composer run verify # run all verifications (check + lint + analyze + test)
