From 43f8f04df305421c9b6aa980a77bb327001e2872 Mon Sep 17 00:00:00 2001 From: Leandro Pedrosa Rodrigues Date: Tue, 16 Sep 2025 21:46:49 -0300 Subject: [PATCH 1/6] build(composer): upgrage for laravel 12 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1867e42..0ce1ba8 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ ], "require": { "php": "^8.1", - "illuminate/contracts": "^10.0||^11", + "illuminate/contracts": "^10.0||^11||^12", "spatie/laravel-package-tools": "^1.14.0" }, "require-dev": { From 8d2eee25ade71c2327de170be7b861ffa5785493 Mon Sep 17 00:00:00 2001 From: Leandro Pedrosa Rodrigues Date: Tue, 16 Sep 2025 21:47:23 -0300 Subject: [PATCH 2/6] ci(test): add laravel 11 and 12 for run-tests in github workflow --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index f7492c5..f94e6f9 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -18,7 +18,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest] php: [8.3, 8.2, 8.1] - laravel: [10.*] + laravel: [10.*, 11.*, 12.*] stability: [prefer-lowest, prefer-stable] include: - laravel: 10.* From 76ba543fe0ce0991b7fc335de501620fd1649f2d Mon Sep 17 00:00:00 2001 From: Leandro Pedrosa Rodrigues Date: Tue, 16 Sep 2025 21:48:20 -0300 Subject: [PATCH 3/6] fix(laravel-api-problem): fix exception AuthorizationException for status code 403 --- src/LaravelApiProblem.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/LaravelApiProblem.php b/src/LaravelApiProblem.php index a1e81d4..a53bb37 100755 --- a/src/LaravelApiProblem.php +++ b/src/LaravelApiProblem.php @@ -4,6 +4,7 @@ namespace Pedrosalpr\LaravelApiProblem; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; @@ -32,6 +33,7 @@ public function __construct( ValidationException::class => $this->validation(), \UnhandledMatchError::class,\Exception::class => $this->default(), HttpException::class => $this->default(419), + AuthorizationException::class => $this->default(403), default => $this->default() }; } From f255a9866cb4ab04cc85e5ea7876219c05ef47f8 Mon Sep 17 00:00:00 2001 From: Leandro Pedrosa Rodrigues Date: Tue, 16 Sep 2025 21:48:44 -0300 Subject: [PATCH 4/6] feat(laravel-api-problem): add method getInstance --- src/Http/LaravelHttpApiProblem.php | 5 +++++ src/LaravelApiProblemInterface.php | 2 ++ 2 files changed, 7 insertions(+) diff --git a/src/Http/LaravelHttpApiProblem.php b/src/Http/LaravelHttpApiProblem.php index fcf6956..5dc8dea 100644 --- a/src/Http/LaravelHttpApiProblem.php +++ b/src/Http/LaravelHttpApiProblem.php @@ -99,6 +99,11 @@ public function getStatusCode(): int return $this->statusCode; } + public function getInstance(): string + { + return $this->instance; + } + public function getExtensions(): array { return $this->extensions; diff --git a/src/LaravelApiProblemInterface.php b/src/LaravelApiProblemInterface.php index d8ae6d1..f62c11a 100644 --- a/src/LaravelApiProblemInterface.php +++ b/src/LaravelApiProblemInterface.php @@ -19,4 +19,6 @@ public function getDetail(): string; public function getExtensions(): array; public function getHeaderProblemJson(): string; + + public function getInstance(): string; } From 703216922d006a9190a672e040db4c90b6a5fae6 Mon Sep 17 00:00:00 2001 From: Leandro Pedrosa Rodrigues Date: Tue, 16 Sep 2025 21:49:22 -0300 Subject: [PATCH 5/6] test(laravel-api-problem): add feature and unit tests, and delete default tests package --- tests/ArchTest.php | 5 - tests/ExampleTest.php | 5 - tests/Feature/ApiProblemTest.php | 132 ++++++++++++++++++++++++ tests/Handlers/TestExceptionHandler.php | 63 +++++++++++ tests/Unit/ApiProblemTest.php | 81 +++++++++++++++ 5 files changed, 276 insertions(+), 10 deletions(-) delete mode 100644 tests/ArchTest.php delete mode 100644 tests/ExampleTest.php create mode 100644 tests/Feature/ApiProblemTest.php create mode 100644 tests/Handlers/TestExceptionHandler.php create mode 100644 tests/Unit/ApiProblemTest.php diff --git a/tests/ArchTest.php b/tests/ArchTest.php deleted file mode 100644 index 87fb64c..0000000 --- a/tests/ArchTest.php +++ /dev/null @@ -1,5 +0,0 @@ -expect(['dd', 'dump', 'ray']) - ->each->not->toBeUsed(); diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php deleted file mode 100644 index 5d36321..0000000 --- a/tests/ExampleTest.php +++ /dev/null @@ -1,5 +0,0 @@ -toBeTrue(); -}); diff --git a/tests/Feature/ApiProblemTest.php b/tests/Feature/ApiProblemTest.php new file mode 100644 index 0000000..0588a2f --- /dev/null +++ b/tests/Feature/ApiProblemTest.php @@ -0,0 +1,132 @@ +app->singleton( + \Illuminate\Contracts\Debug\ExceptionHandler::class, + TestExceptionHandler::class // Use sua classe de handler de teste aqui + ); +}); + +test('authentication exception returns 401 problem json', function () { + // Definir uma rota que lança a exceção diretamente. + Route::get('/api/protected', fn () => throw new AuthenticationException('Unauthenticated.')); + + getJson('/api/protected') + ->assertStatus(Response::HTTP_UNAUTHORIZED) + ->assertHeader('Content-Type', 'application/problem+json') + ->assertJson([ + 'status' => Response::HTTP_UNAUTHORIZED, + 'title' => 'Unauthorized', + 'detail' => 'Unauthenticated.', + ]); +}); + +test('authorization exception returns 403 problem json', function () { + // Definir uma rota que lança a exceção diretamente. + Route::get('/api/forbidden', fn () => throw new AuthorizationException('This action is unauthorized.')); + + getJson('/api/forbidden') + ->assertStatus(Response::HTTP_FORBIDDEN) + ->assertHeader('Content-Type', 'application/problem+json') + ->assertJson([ + 'status' => Response::HTTP_FORBIDDEN, + 'title' => 'Forbidden', + 'detail' => 'This action is unauthorized.', + ]); +}); + +test('validation exception returns 422 problem json', function () { + Route::post('/api/validate', function () { + request()->validate(['email' => 'required|email']); + }); + + postJson('/api/validate', ['email' => 'invalid-email']) + ->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY) + ->assertHeader('Content-Type', 'application/problem+json') + ->assertJson([ + 'status' => Response::HTTP_UNPROCESSABLE_ENTITY, + 'title' => 'Unprocessable Entity', + 'detail' => 'The given data was invalid.', + ]) + ->assertJsonStructure([ + 'status', + 'title', + 'detail', + 'errors' => [ + 'email', + ], + ]); +}); + +test('not found exception returns 404 problem json', function () { + // A rota não é definida, o Laravel lança a exceção automaticamente + getJson('/api/non-existent-route') + ->assertStatus(404) + ->assertHeader('Content-Type', 'application/problem+json') + ->assertJson([ + 'status' => 404, + 'title' => 'Not Found', + 'detail' => 'The route api/non-existent-route could not be found.', + ]); +}); + + + +test('method not allowed exception returns 405 problem json', function () { + // Definir uma rota que só aceita POST + Route::post('/api/only-post', fn () => ['message' => 'success']); + + // Tentar acessar com o método GET + getJson('/api/only-post') + ->assertStatus(Response::HTTP_METHOD_NOT_ALLOWED) + ->assertHeader('Content-Type', 'application/problem+json') + ->assertJson([ + 'status' => Response::HTTP_METHOD_NOT_ALLOWED, + 'title' => 'Method Not Allowed', + 'detail' => 'The GET method is not supported for route api/only-post. Supported methods: POST.', + ]); +}); + +test('generic exception returns 500 problem json', function () { + Route::get('/api/internal-error', function () { + throw new Exception('An internal server error occurred.'); + }); + + getJson('/api/internal-error') + ->assertStatus(500) + ->assertHeader('Content-Type', 'application/problem+json') + ->assertJson([ + 'status' => 500, + 'title' => 'Internal Server Error', + 'detail' => 'An internal server error occurred.', + ]); +}); + + + +test('http exception returns correct problem json', function () { + Route::get('/api/custom-error', function () { + throw new HttpException(Response::HTTP_UNAUTHORIZED, 'You do not have permission.'); + }); + + getJson('/api/custom-error') + ->assertStatus(Response::HTTP_UNAUTHORIZED) + ->assertHeader('Content-Type', 'application/problem+json') + ->assertJson([ + 'status' => Response::HTTP_UNAUTHORIZED, + 'title' => 'Unauthorized', + 'detail' => 'You do not have permission.', + ]); +}); diff --git a/tests/Handlers/TestExceptionHandler.php b/tests/Handlers/TestExceptionHandler.php new file mode 100644 index 0000000..1af2054 --- /dev/null +++ b/tests/Handlers/TestExceptionHandler.php @@ -0,0 +1,63 @@ +fullUrl(); + + if ($e instanceof AuthenticationException) { + $problem = new LaravelHttpApiProblem(Response::HTTP_UNAUTHORIZED, $e->getMessage(), $instance); + return response()->json($problem->toArray(), $problem->getStatusCode()) + ->withHeaders(['Content-Type' => $problem->getHeaderProblemJson()]); + } + + if ($e instanceof AuthorizationException) { + $problem = new LaravelHttpApiProblem(Response::HTTP_FORBIDDEN, $e->getMessage(), $instance); + return response()->json($problem->toArray(), $problem->getStatusCode()) + ->withHeaders(['Content-Type' => $problem->getHeaderProblemJson()]); + } + + // As exceções Http (como 404) precisam ser tratadas para que o cabeçalho seja `application/problem+json` + if ($this->isHttpException($e)) { + $statusCode = $e->getStatusCode(); + $title = Response::$statusTexts[$statusCode] ?? 'Unknown Error'; + $problem = new LaravelHttpApiProblem($statusCode, $e->getMessage(), $instance, title: $title); + return response()->json($problem->toArray(), $problem->getStatusCode()) + ->withHeaders(['Content-Type' => $problem->getHeaderProblemJson()]); + } + + if ($e instanceof ValidationException) { + $problem = new LaravelHttpApiProblem( + statusCode: Response::HTTP_UNPROCESSABLE_ENTITY, + detail: 'The given data was invalid.', + instance: $instance, + extensions: ['errors' => $e->errors()] + ); + return response()->json($problem->toArray(), $problem->getStatusCode()) + ->withHeaders(['Content-Type' => $problem->getHeaderProblemJson()]); + } + + $problem = new LaravelHttpApiProblem( + statusCode: Response::HTTP_INTERNAL_SERVER_ERROR, + detail: 'An internal server error occurred.', // Mensagem genérica por segurança + instance: $instance + ); + + return response()->json($problem->toArray(), $problem->getStatusCode()) + ->withHeaders(['Content-Type' => $problem->getHeaderProblemJson()]); + } +} diff --git a/tests/Unit/ApiProblemTest.php b/tests/Unit/ApiProblemTest.php new file mode 100644 index 0000000..d2321da --- /dev/null +++ b/tests/Unit/ApiProblemTest.php @@ -0,0 +1,81 @@ +getStatusCode())->toBe(404); + expect($problem->getDetail())->toBe('The resource was not found.'); + expect($problem->getInstance())->toBe('https://example.com/api/posts/1'); + expect($problem->getTitle())->toBe('Not Found'); + expect($problem->getType())->toBe('about:blank'); +}); + +test('it sets title and type automatically based on status code', function () { + $problem = new LaravelHttpApiProblem( + statusCode: 401, + detail: 'Authentication failed.', + instance: 'https://example.com/api/protected' + ); + + expect($problem->getTitle())->toBe('Unauthorized'); + expect($problem->getType())->toBe('about:blank'); +}); + +test('toArray method returns valid problem json structure', function () { + $problem = new LaravelHttpApiProblem( + statusCode: 422, + detail: 'The request payload is invalid.', + instance: 'https://example.com/api/users', + extensions: ['errors' => ['name' => ['The name field is required.']]] + ); + + $array = $problem->toArray(); + + expect($array) + ->toHaveKeys(['status', 'type', 'title', 'detail', 'instance', 'timestamp', 'errors']) + ->and($array['status'])->toBe(422) + ->and($array['title'])->toBe('Unprocessable Entity') + ->and($array['errors']['name'])->toBe(['The name field is required.']); +}); + +test('it defaults status code to 400 if out of range', function (int $statusCode) { + $problem = new LaravelHttpApiProblem( + statusCode: $statusCode, + detail: 'Invalid status code.', + instance: 'https://example.com/api' + ); + + expect($problem->getStatusCode())->toBe(400); +})->with([ + 100, + 600, +]); + +test('it includes extensions in the problem detail', function () { + $extensions = [ + 'trace_id' => '1a2b3c4d5e', + 'app_code' => 'AUTH-001' + ]; + + $problem = new LaravelHttpApiProblem( + statusCode: 401, + detail: 'Unauthorized access.', + instance: 'https://example.com/api/protected', + extensions: $extensions + ); + + $array = $problem->toArray(); + + expect($array)->toHaveKeys(['trace_id', 'app_code']); + expect($array['trace_id'])->toBe('1a2b3c4d5e'); +}); From ee4ff2794dc38cccb83851787bbbdf5d1dbd46b9 Mon Sep 17 00:00:00 2001 From: pedrosalpr <5918761+pedrosalpr@users.noreply.github.com> Date: Wed, 17 Sep 2025 00:49:50 +0000 Subject: [PATCH 6/6] Fix styling --- src/Commands/LaravelApiProblemCommand.php | 2 +- .../LaravelApiProblemExceptionCommand.php | 4 +-- src/Http/LaravelHttpApiProblem.php | 4 +-- src/LaravelApiProblem.php | 31 ++++--------------- tests/Feature/ApiProblemTest.php | 10 +++--- tests/Handlers/TestExceptionHandler.php | 8 +++-- tests/Unit/ApiProblemTest.php | 4 +-- 7 files changed, 22 insertions(+), 41 deletions(-) diff --git a/src/Commands/LaravelApiProblemCommand.php b/src/Commands/LaravelApiProblemCommand.php index 202444a..b126421 100644 --- a/src/Commands/LaravelApiProblemCommand.php +++ b/src/Commands/LaravelApiProblemCommand.php @@ -23,7 +23,7 @@ protected function resolveStubPath(string $stub): string { return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) ? $customPath - : __DIR__ . $stub; + : __DIR__.$stub; } protected function getDefaultNamespace($rootNamespace): string diff --git a/src/Commands/LaravelApiProblemExceptionCommand.php b/src/Commands/LaravelApiProblemExceptionCommand.php index 5e88235..9834187 100644 --- a/src/Commands/LaravelApiProblemExceptionCommand.php +++ b/src/Commands/LaravelApiProblemExceptionCommand.php @@ -24,7 +24,7 @@ protected function resolveStubPath(string $stub): string { return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) ? $customPath - : __DIR__ . $stub; + : __DIR__.$stub; } protected function getDefaultNamespace($rootNamespace): string @@ -34,7 +34,7 @@ protected function getDefaultNamespace($rootNamespace): string protected function replaceClass($stub, $name) { - $class = str_replace($this->getNamespace($name) . '\\', '', $name); + $class = str_replace($this->getNamespace($name).'\\', '', $name); // Do string replacement return str_replace('{{ class }}', $class, $stub); diff --git a/src/Http/LaravelHttpApiProblem.php b/src/Http/LaravelHttpApiProblem.php index 5dc8dea..d98bb19 100644 --- a/src/Http/LaravelHttpApiProblem.php +++ b/src/Http/LaravelHttpApiProblem.php @@ -72,7 +72,7 @@ public function __construct( if ($this->statusCode < 400 || $this->statusCode > 599) { $this->statusCode = 400; } - if (!filter_var($this->type, FILTER_VALIDATE_URL) || empty($this->title)) { + if (! filter_var($this->type, FILTER_VALIDATE_URL) || empty($this->title)) { $this->title = $this->getTitleForStatusCode($this->statusCode); $this->type = self::TYPE_ABOUT_BLANK; } @@ -118,7 +118,7 @@ public function toArray(): array 'title' => $this->title, 'detail' => $this->detail, 'instance' => $this->instance, - 'timestamp' => $this->timestamp->toJSON() + 'timestamp' => $this->timestamp->toJSON(), ], $this->extensions ); diff --git a/src/LaravelApiProblem.php b/src/LaravelApiProblem.php index a53bb37..b940fc8 100755 --- a/src/LaravelApiProblem.php +++ b/src/LaravelApiProblem.php @@ -27,6 +27,7 @@ public function __construct( if ($exception instanceof LaravelApiProblemException) { $this->apiProblemException = $exception; $this->apiProblemException(); + return; } match (get_class($exception)) { @@ -40,8 +41,6 @@ public function __construct( /** * Render the exception as an HTTP response. - * - * @return JsonResponse */ public function render(): JsonResponse { @@ -56,8 +55,6 @@ public function render(): JsonResponse /** * Debug the class in array to view more details, such as: api problem and exception - * - * @return array */ public function toDebuggableArray(): array { @@ -82,8 +79,6 @@ public function toDebuggableArray(): array /** * Transform any exception into an http api problem with status code - * - * @param null|int $statusCode */ protected function default(?int $statusCode = null): void { @@ -105,7 +100,7 @@ protected function validation(): void $extensions = [ 'errors' => ($this->exception instanceof ValidationException) ? $this->exception->errors() - : null + : null, ]; $this->apiProblem = new LaravelHttpApiProblem( Response::HTTP_UNPROCESSABLE_ENTITY, @@ -132,8 +127,6 @@ protected function apiProblemException(): void /** * Get uri as instance - * - * @return string */ protected function getUriInstance(): string { @@ -142,30 +135,25 @@ protected function getUriInstance(): string /** * Get the context if it exists within the exception and return it as an extension - * - * @return array */ protected function getContextExceptionAsExtensions(): array { $extensions = []; - if (!method_exists($this->exception, 'context')) { + if (! method_exists($this->exception, 'context')) { return $extensions; } $context = $this->exception->context(); if (is_array($context)) { $extensions = $context; - } elseif (!empty($context)) { + } elseif (! empty($context)) { $extensions = [$context]; } + return $extensions; } /** * Gets the status code from the exception code, or from the HttpException Interface, otherwise it returns an Internal Server Error - * - * @param null|int $code - * - * @return int */ protected function getStatusCode(?int $code): int { @@ -177,6 +165,7 @@ protected function getStatusCode(?int $code): int ? $this->exception->getStatusCode() : Response::HTTP_INTERNAL_SERVER_ERROR; } + return ($this->isStatusCodeInternalOrServerError($this->exception->getCode())) ? $this->exception->getCode() : Response::HTTP_INTERNAL_SERVER_ERROR; @@ -184,10 +173,6 @@ protected function getStatusCode(?int $code): int /** * Checks if the status code is of the integer type and is in the range of Client and Server Errors - * - * @param null|int $statusCode - * - * @return bool */ protected function isStatusCodeInternalOrServerError(?int $statusCode): bool { @@ -196,10 +181,6 @@ protected function isStatusCodeInternalOrServerError(?int $statusCode): bool /** * Serialize the exception into an array - * - * @param \Throwable $throwable - * - * @return array */ private function serializeException(\Throwable $throwable): array { diff --git a/tests/Feature/ApiProblemTest.php b/tests/Feature/ApiProblemTest.php index 0588a2f..613bb32 100644 --- a/tests/Feature/ApiProblemTest.php +++ b/tests/Feature/ApiProblemTest.php @@ -2,14 +2,16 @@ declare(strict_types=1); -use function Pest\Laravel\{getJson, postJson}; -use Illuminate\Auth\AuthenticationException; use Illuminate\Auth\Access\AuthorizationException; +use Illuminate\Auth\AuthenticationException; use Illuminate\Http\Response; use Illuminate\Support\Facades\Route; use Pedrosalpr\LaravelApiProblem\Tests\Handlers\TestExceptionHandler; use Symfony\Component\HttpKernel\Exception\HttpException; +use function Pest\Laravel\getJson; +use function Pest\Laravel\postJson; + // Antes de cada teste, substitua o manipulador de exceções do Laravel // pelo seu manipulador de exceções de teste. beforeEach(function () { @@ -82,8 +84,6 @@ ]); }); - - test('method not allowed exception returns 405 problem json', function () { // Definir uma rota que só aceita POST Route::post('/api/only-post', fn () => ['message' => 'success']); @@ -114,8 +114,6 @@ ]); }); - - test('http exception returns correct problem json', function () { Route::get('/api/custom-error', function () { throw new HttpException(Response::HTTP_UNAUTHORIZED, 'You do not have permission.'); diff --git a/tests/Handlers/TestExceptionHandler.php b/tests/Handlers/TestExceptionHandler.php index 1af2054..2cc6cfc 100644 --- a/tests/Handlers/TestExceptionHandler.php +++ b/tests/Handlers/TestExceptionHandler.php @@ -4,10 +4,10 @@ namespace Pedrosalpr\LaravelApiProblem\Tests\Handlers; +use Illuminate\Auth\Access\AuthorizationException; +use Illuminate\Auth\AuthenticationException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Http\Response; -use Illuminate\Auth\AuthenticationException; -use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Validation\ValidationException; use Pedrosalpr\LaravelApiProblem\Http\LaravelHttpApiProblem; use Throwable; @@ -21,12 +21,14 @@ public function render($request, Throwable $e) if ($e instanceof AuthenticationException) { $problem = new LaravelHttpApiProblem(Response::HTTP_UNAUTHORIZED, $e->getMessage(), $instance); + return response()->json($problem->toArray(), $problem->getStatusCode()) ->withHeaders(['Content-Type' => $problem->getHeaderProblemJson()]); } if ($e instanceof AuthorizationException) { $problem = new LaravelHttpApiProblem(Response::HTTP_FORBIDDEN, $e->getMessage(), $instance); + return response()->json($problem->toArray(), $problem->getStatusCode()) ->withHeaders(['Content-Type' => $problem->getHeaderProblemJson()]); } @@ -36,6 +38,7 @@ public function render($request, Throwable $e) $statusCode = $e->getStatusCode(); $title = Response::$statusTexts[$statusCode] ?? 'Unknown Error'; $problem = new LaravelHttpApiProblem($statusCode, $e->getMessage(), $instance, title: $title); + return response()->json($problem->toArray(), $problem->getStatusCode()) ->withHeaders(['Content-Type' => $problem->getHeaderProblemJson()]); } @@ -47,6 +50,7 @@ public function render($request, Throwable $e) instance: $instance, extensions: ['errors' => $e->errors()] ); + return response()->json($problem->toArray(), $problem->getStatusCode()) ->withHeaders(['Content-Type' => $problem->getHeaderProblemJson()]); } diff --git a/tests/Unit/ApiProblemTest.php b/tests/Unit/ApiProblemTest.php index d2321da..05c346d 100644 --- a/tests/Unit/ApiProblemTest.php +++ b/tests/Unit/ApiProblemTest.php @@ -3,8 +3,6 @@ declare(strict_types=1); use Pedrosalpr\LaravelApiProblem\Http\LaravelHttpApiProblem; -use Pedrosalpr\LaravelApiProblem\Tests\TestCase; -use function Pest\Laravel\{getJson, postJson}; test('it can be instantiated with required arguments', function () { $problem = new LaravelHttpApiProblem( @@ -64,7 +62,7 @@ test('it includes extensions in the problem detail', function () { $extensions = [ 'trace_id' => '1a2b3c4d5e', - 'app_code' => 'AUTH-001' + 'app_code' => 'AUTH-001', ]; $problem = new LaravelHttpApiProblem(