phpro/api-problem-bundle
Symfony bundle that turns exceptions into RFC7807 Problem Details responses (application/problem+json). Listens for ApiProblemException/HttpException/Security exceptions and converts them to standardized JSON errors based on phpro/api-problem, with support for custom transformers.
Installation
composer require phpro/api-problem-bundle
Add to config/app.php (Laravel) or config/bundles.php (Symfony):
// Laravel (no XML config support in 1.12.1+)
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',
'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.']],
]);
Global Exception Handling (Laravel) Replace Symfony listeners with middleware (1.12.1+ recommended):
// app/Http/Middleware/HandleApiProblems.php
public function handle($request, Closure $next)
{
try {
return $next($request);
} catch (ProblemException $e) {
return response()->json($e->getProblem(), $e->getStatusCode());
}
}
Custom Problem Types
Register factories in config/api_problem.php (no XML support):
'factories' => [
'auth_error' => [
'class' => \App\Problem\AuthProblemFactory::class,
'status' => 401,
'type' => 'https://example.com/probs/auth-error',
],
],
Linking Problems
Add links for API documentation:
throw new ProblemException('Rate limit exceeded', 429, [
'links' => [
['href' => '/docs/rate-limits', 'rel' => 'documentation'],
],
]);
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'
XML Config Removed
api_problem.xml) is dropped in 1.12.1. Use PHP config (config/api_problem.php) only.// Old (XML)
<problem type="auth_error" status="401" />
// New (PHP)
'factories' => [
'auth_error' => [
'status' => 401,
'type' => 'https://example.com/probs/auth-error',
],
],
Symfony Listeners Deprecated
ProblemResponseListener is Symfony-specific. In Laravel, always use middleware for exception handling.Status Code Defaults
ProblemException defaults to 500 if no status is provided. Always specify:
// ❌ Avoid
throw new ProblemException('Error');
// ✅ Correct
throw new ProblemException('Error', 404);
Type URIs Must Be Absolute
https://example.com/probs/...) to avoid collisions in multi-environment APIs.Content-Type: application/problem+json is set. Laravel’s response()->json() handles this automatically.config/api_problem.php.Problem::create()->withDebug() to include stack traces:
Problem::create('Oops')
->setStatus(500)
->withDebug()
->throw();
Custom Problem Classes
Extend Problem for domain logic:
class PaymentProblem extends Problem
{
public function setTransactionId(string $id): self
{
$this->set('transaction_id', $id);
return $this;
}
}
Middleware for Global Metadata Add fields to all problems:
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 Problems Mock problems in tests:
$problem = Problem::create('Test')
->setStatus(400)
->setType('https://example.com/probs/test');
$this->expectException(ProblemException::class);
$this->expectExceptionMessage($problem->getDetail());
Problem Transformers Convert problems to other formats (e.g., XML) via custom middleware or listeners.
How can I help you explore Laravel packages today?