Installation Add the bundle via Composer:
composer require dosfarma/exceptions-bundle
Register the bundle in config/bundles.php (Symfony) or config/app.php (Laravel via Symfony bridge):
return [
// ...
DosFarma\ExceptionsBundle\DosFarmaExceptionsBundle::class => ['all' => true],
];
First Use Case Throw an exception in your controller or service:
use DosFarma\Exceptions\ApiException;
throw new ApiException('Invalid request', 40001123);
The bundle automatically converts it to a JsonResponse with the specified structure.
Where to Look First
config/packages/dosfarma_exceptions.yaml (Symfony) or config/exceptions.php (Laravel).ApiResponseLoader service (see below).src/Exception/ApiException.php for available exception types.Throwing Exceptions
Use ApiException for API-specific errors:
throw new ApiException('User not found', 404001, ['user_id' => $request->user_id]);
The payload will include:
{
"message": "User not found",
"error_code": 404001,
"details": { "user_id": 123 }
}
Customizing Responses
Extend the default ApiResponseLoader to modify the JSON structure:
namespace App\Service;
use DosFarma\ExceptionsBundle\Http\Service\ApiResponseLoader as BaseLoader;
use Symfony\Component\HttpFoundation\JsonResponse;
class CustomApiResponseLoader extends BaseLoader
{
public function createResponse(string $message, int $errorCode, array $details = []): JsonResponse
{
return new JsonResponse([
'status' => 'error',
'code' => $errorCode,
'message' => $message,
'data' => $details,
'timestamp' => now()->toIso8601String(),
]);
}
}
Register it in config/services.yaml (Symfony) or config/app.php (Laravel):
services:
DosFarma\ExceptionsBundle\Http\Service\ApiResponseLoader:
class: App\Service\CustomApiResponseLoader
Integration with Laravel If using Laravel, create a service provider to bind the bundle’s services:
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use DosFarma\ExceptionsBundle\Http\Service\ApiResponseLoader;
class ExceptionsServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(ApiResponseLoader::class, function ($app) {
return new \App\Service\CustomApiResponseLoader();
});
}
}
Handling Non-API Exceptions For non-API routes, exclude the listener by tagging the controller:
#[Route('/non-api', name: 'non_api_route', methods: ['GET'])]
class NonApiController extends AbstractController
{
public function __construct()
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
}
}
Or disable globally in config:
dosfarma_exceptions:
enabled: false
Double Exception Handling
If using Laravel’s built-in exception handler (e.g., App\Exceptions\Handler), ensure it doesn’t interfere with the bundle’s ApiExceptionListener. Disable Laravel’s JSON responses for ApiException:
public function render($request, Throwable $exception)
{
if ($exception instanceof \DosFarma\Exceptions\ApiException) {
return parent::render($request, $exception); // Let the bundle handle it
}
// ... rest of your logic
}
HTTP Code Mismatch
The bundle uses the error_code (e.g., 40001123) as the HTTP status code by default. Override this behavior in CustomApiResponseLoader:
public function createResponse(string $message, int $errorCode, array $details = []): JsonResponse
{
$httpCode = $this->mapErrorCodeToHttpStatus($errorCode);
return new JsonResponse(/* ... */, $httpCode);
}
Service ID Conflicts
Ensure the service ID DosFarma\ExceptionsBundle\Http\Service\ApiResponseLoader is unique in your container. Avoid naming collisions with other bundles.
Log Unhandled Exceptions Add a subscriber to log exceptions before the listener processes them:
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
$eventDispatcher->addListener(KernelEvents::EXCEPTION, function (ExceptionEvent $event) {
if ($event->getThrowable() instanceof \DosFarma\Exceptions\ApiException) {
\Log::error('API Exception', [
'message' => $event->getThrowable()->getMessage(),
'code' => $event->getThrowable()->getCode(),
]);
}
});
Test the Listener Verify the listener is active by throwing an exception in a test:
public function testApiExceptionHandling()
{
$this->expectException(\Symfony\Component\HttpKernel\Exception\HttpException::class);
$this->throwException(new ApiException('Test', 500001));
$this->assertResponseStatusCode(500); // Or custom HTTP code
}
Custom Exception Types
Extend ApiException to add domain-specific logic:
namespace App\Exception;
use DosFarma\Exceptions\ApiException;
class ValidationApiException extends ApiException
{
public function __construct(array $errors, int $code = 400000)
{
parent::__construct('Validation failed', $code, ['errors' => $errors]);
}
}
Dynamic Error Codes Use a service to generate error codes dynamically:
namespace App\Service;
class ErrorCodeGenerator
{
public function generate(string $context, string $action): int
{
return hash('crc32b', "$context-$action") & 0xFFFFFFFF;
}
}
Inject it into your exceptions:
$errorCode = $this->errorCodeGenerator->generate('user', 'not_found');
throw new ApiException('User not found', $errorCode);
Localization Support
Override the ApiResponseLoader to support localized messages:
public function createResponse(string $message, int $errorCode, array $details = []): JsonResponse
{
$translator = $this->container->get('translator');
$translatedMessage = $translator->trans($message);
return new JsonResponse([
'message' => $translatedMessage,
// ...
]);
}
How can I help you explore Laravel packages today?