Installation
composer require phpro/api-problem-bundle
Add to config/bundles.php (Symfony) or config/app.php (Laravel via bridge):
Phpro\ApiProblemBundle\PhproApiProblemBundle::class => ['all' => true],
First Use Case Throw a problem in a controller:
use Phpro\ApiProblemBundle\Exception\ProblemException;
public function show(User $user)
{
if (!$user->isActive()) {
throw new ProblemException('User is inactive', 403, [
'type' => 'https://example.com/probs/inactive-user',
'status' => '403',
'title' => 'Inactive User',
'detail' => 'The requested user is inactive',
]);
}
return $user;
}
Where to Look First
Validation Errors
Use ProblemException with validation_errors type:
throw new ProblemException('Validation failed', 422, [
'type' => 'https://example.com/probs/validation-error',
'errors' => ['email' => ['The email field is required.']],
]);
API Gateway Integration Catch exceptions globally and convert to problems:
// Laravel middleware (app/Http/Middleware/ConvertExceptions.php)
public function handle($request, Closure $next)
{
try {
return $next($request);
} catch (ProblemException $e) {
return response()->json($e->getProblem(), $e->getStatusCode());
} catch (\Exception $e) {
return response()->json(
Problem::create($e->getMessage())
->setStatus($e->getCode() ?: 500)
->setType('https://example.com/probs/unexpected-error')
);
}
}
Custom Problem Types
Extend ProblemFactory for domain-specific problems:
// config/api_problem.php
'factories' => [
'authentication_error' => [
'class' => \App\Problem\AuthProblemFactory::class,
'status' => 401,
'type' => 'https://example.com/probs/auth-error',
],
],
Linking Problems
Add links to problems for API documentation or remediation:
throw new ProblemException('Rate limit exceeded', 429, [
'links' => [
['href' => '/docs/rate-limits', 'rel' => 'documentation'],
['href' => '/auth/refresh', 'rel' => 'retry-after'],
],
]);
ProblemException in HandleInvalidUserInput:
public function invalid($request, $validator, $customMessages)
{
throw new ProblemException('Validation failed', 422, [
'errors' => $validator->errors()->toArray(),
]);
}
components:
schemas:
Problem:
$ref: 'https://tools.ietf.org/html/rfc7807'
Status Code Overrides
ProblemException defaults to 500 if no status is provided. Always specify:
// ❌ Avoid
throw new ProblemException('Error');
// ✅ Correct
throw new ProblemException('Error', 404);
Symfony-Specific Listeners
ProblemResponseListener for Symfony. In Laravel, manually handle exceptions (see Implementation Patterns).Nested Problems
errors or details arrays. Keep payloads flat for readability:
// ❌ Too nested
{ "errors": { "user": { "email": ["Invalid"] } } }
// ✅ Flat
{ "errors": { "email": ["Invalid"] } }
Type URIs
https://example.com/probs/...) for type fields to avoid collisions. Relative paths may break if APIs are versioned.Content-Type: application/problem+json is set in responses. Laravel’s response()->json() handles this automatically.config/api_problem.php.Problem::create()->withDebug() to include stack traces in development:
Problem::create('Oops')
->setStatus(500)
->withDebug()
->throw();
Custom Problem Classes
Extend Problem for domain-specific logic:
class PaymentProblem extends Problem
{
public function setTransactionId(string $id): self
{
$this->set('transaction_id', $id);
return $this;
}
}
Problem Transformers Convert problems to other formats (e.g., XML) via events:
// Listen to `api_problem.response` (Symfony) or adapt for Laravel
$problem->addExtension('custom_field', 'value');
Problem Middleware Add metadata to problems globally:
// Laravel middleware
public function handle($request, Closure $next)
{
$response = $next($request);
if ($response->exception instanceof ProblemException) {
$problem = $response->exception->getProblem();
$problem->set('request_id', $request->header('X-Request-ID'));
$response->setContent(json_encode($problem));
}
return $response;
}
Testing Mock problems in tests:
$problem = Problem::create('Test')
->setStatus(400)
->setType('https://example.com/probs/test');
$this->expectException(ProblemException::class);
$this->expectExceptionMessage($problem->getDetail());
How can I help you explore Laravel packages today?