Installation:
composer require phpro/api-problem-bundle
Add the bundle to config/bundles.php if not using Symfony Flex:
Phpro\ApiProblemBundle\ApiProblemBundle::class => ['all' => true],
First Use Case:
Throw an ApiProblemException in a controller to return RFC7807-compliant error responses.
use Phpro\ApiProblem\Exception\ApiProblemException;
public function createAction() {
throw new ApiProblemException(
new \Phpro\ApiProblem\Problem(
'https://example.com/probs/out-of-credit',
'You do not have enough credit.',
'https://example.com/docs/errors/out-of-credit'
)
);
}
Where to Look First:
phpro/api-problem (core models).ExceptionListener interface (for customization).Error Handling:
Replace generic exceptions with ApiProblemException for structured errors:
throw new ApiProblemException(
new \Phpro\ApiProblem\Problem(
'invalid_request',
'Invalid input data',
'https://api.example.com/docs/errors/invalid_request',
'https://example.com/probs/invalid-request',
400,
['field' => 'email', 'reason' => 'invalid format']
)
);
Controller Integration:
Use _format="json" in routes to ensure JSON responses:
#[Route('/api/users', name: 'create_user', methods: ['POST'], defaults: ['_format' => 'json'])]
public function createUser(Request $request) {
// ...
}
Validation Errors:
Convert Symfony Validator errors to ApiProblem:
use Symfony\Component\Validator\ConstraintViolationListInterface;
public function validateAndThrow(ConstraintViolationListInterface $violations) {
$problems = [];
foreach ($violations as $violation) {
$problems[] = new \Phpro\ApiProblem\Problem(
'validation_error',
$violation->getMessage(),
null,
null,
400,
['property' => $violation->getPropertyPath()]
);
}
throw new ApiProblemException($problems);
}
API Problem Types:
Extend \Phpro\ApiProblem\Problem for custom error types:
class PaymentFailedProblem extends \Phpro\ApiProblem\Problem {
public function __construct(string $detail, array $extensions = []) {
parent::__construct(
'payment_failed',
$detail,
'https://api.example.com/docs/errors/payment_failed',
null,
402,
$extensions
);
}
}
Global Exception Handling:
Override the default listener in config/packages/phpro_api_problem.yaml:
phpro_api_problem:
exception_listener:
enabled: true
order: 100 # Priority
Format Mismatch:
_format is missing or incorrect (e.g., _format="xml"), the bundle may not trigger._format="json" or set Accept: application/problem+json.Exception Order:
ExceptionListener runs after Symfony’s default listener.order in config if other listeners interfere.Nested Problems:
ApiProblemException accepts single or array of \Phpro\ApiProblem\Problem objects.throw new ApiProblemException([$problem1, $problem2]);
HTTP Status Codes:
Problem if ApiProblemException uses a default HTTP code.HttpApiProblem for explicit status codes:
throw new ApiProblemException(
new \Phpro\ApiProblem\HttpApiProblem(403, 'Forbidden')
);
Check Response Headers:
Content-Type: application/problem+json
dd($exception) in a custom listener to inspect the Problem object.Log Unhandled Exceptions:
ApiProblemException in a try-catch to log details:
try {
// Risky operation
} catch (ApiProblemException $e) {
$this->logger->error('API Error', ['problem' => $e->getProblem()]);
throw $e;
}
Custom Listener:
use Phpro\ApiProblemBundle\EventListener\ApiProblemExceptionListener;
class CustomApiProblemListener extends ApiProblemExceptionListener {
public function onKernelException(GetResponseForExceptionEvent $event) {
$exception = $event->getThrowable();
if ($exception instanceof ApiProblemException) {
$problem = $exception->getProblem();
$problem->with('debug', app()->environment() === 'dev');
$event->setResponse($this->createResponse($problem));
}
}
}
services.yaml:
services:
App\EventListener\CustomApiProblemListener:
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException, priority: 100 }
Problem Factories:
class ValidationProblemFactory {
public static function createFromViolations(ConstraintViolationListInterface $violations): array {
return array_map(function ($violation) {
return new \Phpro\ApiProblem\Problem(
'validation_error',
$violation->getMessage(),
null,
null,
400,
['property' => $violation->getPropertyPath()]
);
}, iterator_to_array($violations));
}
}
Dynamic Problem Types:
trait ProblemTypes {
protected function createNotFoundProblem(string $id): \Phpro\ApiProblem\Problem {
return new \Phpro\ApiProblem\Problem(
'not_found',
sprintf('Resource %s not found', $id),
null,
null,
404
);
}
}
Testing:
$response = $this->client->get('/api/protected');
$this->assertEquals(401, $response->getStatusCode());
$this->assertEquals('application/problem+json', $response->headers->get('Content-Type'));
$data = json_decode($response->getContent(), true);
$this->assertArrayHasKey('type', $data);
How can I help you explore Laravel packages today?