diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..4b37c43 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,50 @@ +name: Tests + +on: + push: + branches: [2.0, 3.0, main, master] + pull_request: + branches: [2.0, 3.0, main, master] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + fail-fast: true + matrix: + php: [8.1, 8.2, 8.3] + laravel: [10.*, 11.*, 12.*] + include: + - laravel: 10.* + testbench: 8.* + - laravel: 11.* + testbench: 9.* + - laravel: 12.* + testbench: 10.* + exclude: + - php: 8.1 + laravel: 11.* + - php: 8.1 + laravel: 12.* + + name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite + coverage: none + + - name: Install dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update + composer update --prefer-dist --no-interaction + + - name: Execute tests + run: vendor/bin/phpunit diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 9a78174..55c943e 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -18,11 +18,14 @@ checks: fix_doc_comments: true build: + environment: + php: + version: 8.2 tests: override: - php-scrutinizer-run - - command: 'vendor/bin/phpunit --coverage-clover=some-file' + command: 'vendor/bin/phpunit --coverage-clover=coverage.xml' coverage: - file: 'some-file' + file: 'coverage.xml' format: 'clover' diff --git a/.styleci.yml b/.styleci.yml index 473c31a..9b6f8d9 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -1,7 +1,9 @@ preset: laravel +risky: true + finder: exclude: - - "tests" + - tests name: - "*.php" diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b62eb3c..0000000 --- a/.travis.yml +++ /dev/null @@ -1,59 +0,0 @@ -language: php - -env: - global: - - setup=stable - -matrix: - include: - - php: 7.0 - env: - - testbench=3.4.x - - phpunit=5.7.x - - php: 7.0 - env: - - testbench=3.5.x - - phpunit=6.0.x - - php: 7.1 - env: - - testbench=3.5.x - - phpunit=6.0.x - - php: 7.1 - env: - - testbench=3.6.x - - phpunit=7.0.x - - php: 7.1 - env: - - testbench=3.6.x - - phpunit=7.0.x - - php: 7.1 - env: - - testbench=3.7.x - - phpunit=7.0.x - - php: 7.2 - env: - - testbench=3.5.x - - phpunit=6.0.x - - php: 7.2 - env: - - testbench=3.6.x - - phpunit=7.0.x - - php: 7.2 - env: - - testbench=3.6.x - - phpunit=7.0.x - - php: 7.2 - env: - - testbench=3.7.x - - phpunit=7.0.x - -sudo: false - -install: - - composer require orchestra/testbench:${testbench} --dev --no-update - - composer require phpunit/phpunit:${phpunit} --dev --no-update - - if [[ $setup = 'stable' ]]; then travis_retry composer update --prefer-dist --no-interaction --prefer-stable; fi - - if [[ $setup = 'lowest' ]]; then travis_retry composer update --prefer-dist --no-interaction --prefer-lowest --prefer-stable; fi - -script: - - vendor/bin/phpunit diff --git a/README.md b/README.md index 2f5277f..6ed4112 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,20 @@ # Laravel Json Exception Handler -[![StyleCI](https://styleci.io/repos/101529653/shield?style=plastic&branch=2.0)](https://styleci.io/repos/101529653?style=plastic&branch=2.0) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/sfelix-martins/json-exception-handler/badges/quality-score.png?b=2.0)](https://scrutinizer-ci.com/g/sfelix-martins/json-exception-handler/?branch=2.0) -[![Build Status](https://travis-ci.org/sfelix-martins/json-exception-handler.svg?branch=2.0)](https://travis-ci.org/sfelix-martins/json-exception-handler) +[![StyleCI](https://styleci.io/repos/101529653/shield?style=plastic&branch=3.0)](https://styleci.io/repos/101529653?style=plastic&branch=3.0) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/sfelix-martins/json-exception-handler/badges/quality-score.png?b=3.0)](https://scrutinizer-ci.com/g/sfelix-martins/json-exception-handler/?branch=3.0) +[![Tests](https://github.com/sfelix-martins/json-exception-handler/actions/workflows/tests.yml/badge.svg)](https://github.com/sfelix-martins/json-exception-handler/actions/workflows/tests.yml) Adds methods to your `App\Exceptions\Handler` to treat json responses. It is most useful if you are building APIs! ## Requirements -* Laravel Framework >= 5.4 -* php >= 7.0 +* PHP >= 8.1 +* Laravel Framework 10.x, 11.x, or 12.x ## JsonAPI -Using [JsonAPI](http://jsonapi.org) standard to responses! +Using [JsonAPI](http://jsonapi.org) standard to responses! ## Features @@ -76,26 +76,18 @@ To `Illuminate\Validation\ValidationException`: ## Installing and configuring -Install the package +Install the package ```console $ composer require sfelix-martins/json-exception-handler ``` -If you are not using **Laravel 5.5** version add the `JsonHandlerServiceProvider` to your `config/app.php` providers array: - -```php - 'providers' => [ - ... - SMartins\Exceptions\JsonHandlerServiceProvider::class, - ], -``` +The package uses Laravel's auto-discovery feature, so the service provider will be automatically registered. Publish the config to set your own exception codes ```sh - -$ php artisan vendor:publish --provider="SMartins\JsonHandler\JsonHandlerServiceProvider" +$ php artisan vendor:publish --provider="SMartins\Exceptions\JsonHandlerServiceProvider" ``` Set your exception codes on `config/json-exception-handler.php` on codes array. @@ -115,12 +107,12 @@ In `resources/lang/vendor/exception/lang/$locale` in `exceptions` file you can s ## Using -Use the trait on your `App\Exception\Handler` and add method `jsonResponse()` -passing the `$exception` if `$request` expects a json response on `render()`method +Use the trait on your `App\Exception\Handler` and add method `jsonResponse()` +passing the `$exception` if `$request` expects a json response on `render()` method ```php - use SMartins\Exceptions\JsonHandler; +use Throwable; class Handler extends ExceptionHandler { @@ -128,15 +120,15 @@ class Handler extends ExceptionHandler // ... - public function render($request, Exception $exception) - { + public function render($request, Throwable $e) + { if ($request->expectsJson()) { - return $this->jsonResponse($exception); + return $this->jsonResponse($e); } - return parent::render($request, $exception); + return parent::render($request, $e); } - + // ... ``` @@ -173,7 +165,7 @@ class UserController extends Controller { // If not found the default response is called $user = User::findOrFail($id); - + // Gate define on AuthServiceProvider // Generate an AuthorizationException if fail $this->authorize('users.view', $user->id); @@ -183,54 +175,37 @@ class UserController extends Controller ## Extending -You can too create your own handler to any Exception. E.g.: +You can create your own handler for any Exception. E.g.: -- Create a Handler class that extends of `AbstractHandler`: +- Create a Handler class that extends `AbstractHandler`: ```php namespace App\Exceptions; use GuzzleHttp\Exception\ClientException; use SMartins\Exceptions\Handlers\AbstractHandler; +use SMartins\Exceptions\JsonApi\Error; +use SMartins\Exceptions\JsonApi\Source; +use SMartins\Exceptions\Response\ErrorHandledCollectionInterface; +use SMartins\Exceptions\Response\ErrorHandledInterface; class GuzzleClientHandler extends AbstractHandler { - /** - * Create instance using the Exception to be handled. - * - * @param \GuzzleHttp\Exception\ClientException $e - */ public function __construct(ClientException $e) { parent::__construct($e); } -} -``` - -- You must implements the method `handle()` from `AbstractHandler` class. The method must return an instance of `Error` or `ErrorCollection`: - -```php -namespace App\Exceptions; - -use SMartins\Exceptions\JsonAPI\Error; -use SMartins\Exceptions\JsonAPI\Source; -use GuzzleHttp\Exception\ClientException; -use SMartins\Exceptions\Handlers\AbstractHandler; -class GuzzleClientHandler extends AbstractHandler -{ - // ... - - public function handle() + public function handle(): ErrorHandledInterface|ErrorHandledCollectionInterface { - return (new Error)->setStatus($this->getStatusCode()) - ->setCode($this->getCode()) + return (new Error)->setStatus((string) $this->getStatusCode()) + ->setCode((string) $this->getCode()) ->setSource((new Source())->setPointer($this->getDefaultPointer())) ->setTitle($this->getDefaultTitle()) ->setDetail($this->exception->getMessage()); } - public function getCode() + public function getCode(string $type = 'default'): int|string { // You can add a new type of code on `config/json-exception-handlers.php` return config('json-exception-handler.codes.client.default'); @@ -238,13 +213,17 @@ class GuzzleClientHandler extends AbstractHandler } ``` +- For returning multiple errors: + ```php namespace App\Exceptions; -use SMartins\Exceptions\JsonAPI\Error; -use SMartins\Exceptions\JsonAPI\Source; -use SMartins\Exceptions\JsonAPI\ErrorCollection; +use SMartins\Exceptions\JsonApi\Error; +use SMartins\Exceptions\JsonApi\Source; +use SMartins\Exceptions\JsonApi\ErrorCollection; use SMartins\Exceptions\Handlers\AbstractHandler; +use SMartins\Exceptions\Response\ErrorHandledCollectionInterface; +use SMartins\Exceptions\Response\ErrorHandledInterface; class MyCustomizedHandler extends AbstractHandler { @@ -253,14 +232,14 @@ class MyCustomizedHandler extends AbstractHandler parent::__construct($e); } - public function handle() + public function handle(): ErrorHandledInterface|ErrorHandledCollectionInterface { - $errors = (new ErrorCollection)->setStatusCode(400); + $errors = (new ErrorCollection)->setStatusCode('400'); $exceptions = $this->exception->getExceptions(); foreach ($exceptions as $exception) { - $error = (new Error)->setStatus(422) + $error = (new Error)->setStatus('422') ->setSource((new Source())->setPointer($this->getDefaultPointer())) ->setTitle($this->getDefaultTitle()) ->setDetail($exception->getMessage()); @@ -273,15 +252,15 @@ class MyCustomizedHandler extends AbstractHandler } ``` -- Now just registry your customized handler on `App\Exception\Handler` file on attribute `exceptionHandlers`. E.g: +- Now just register your customized handler on `App\Exception\Handler` file on attribute `exceptionHandlers`. E.g: ```php namespace App\Exceptions; -use Exception; use GuzzleHttp\Exception\ClientException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use SMartins\Exceptions\JsonHandler; +use Throwable; class Handler extends ExceptionHandler { @@ -291,9 +270,18 @@ class Handler extends ExceptionHandler // Set on key the exception and on value the handler. ClientException::class => GuzzleClientHandler::class, ]; - ``` +## Upgrading from 2.x + +If you're upgrading from version 2.x, note the following changes: + +1. PHP 8.1+ is now required +2. Laravel 10, 11, or 12 is required +3. The `render()` method signature now uses `Throwable` instead of `Exception` +4. All handler classes now use strict types and proper return type declarations +5. Status codes are now always strings in the JSON response (as per JsonAPI spec) + ## Response References: - http://jsonapi.org/format/#errors diff --git a/composer.json b/composer.json index 17a91b8..f84a343 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,12 @@ } ], "require": { - "php": ">=7.0.0" + "php": "^8.1", + "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/http": "^10.0|^11.0|^12.0", + "illuminate/validation": "^10.0|^11.0|^12.0", + "illuminate/database": "^10.0|^11.0|^12.0", + "illuminate/auth": "^10.0|^11.0|^12.0" }, "autoload": { "psr-4": { @@ -34,7 +39,9 @@ } }, "require-dev": { - "orchestra/testbench": "~3.0", - "phpunit/phpunit": "^7.1" - } + "orchestra/testbench": "^8.0|^9.0|^10.0", + "phpunit/phpunit": "^10.0|^11.0" + }, + "minimum-stability": "stable", + "prefer-stable": true } diff --git a/phpunit.xml b/phpunit.xml index c31b987..13afc55 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,24 +1,23 @@ - ./tests/ - - + + ./src/ - - + + diff --git a/src/Handlers/AbstractHandler.php b/src/Handlers/AbstractHandler.php index 31508a2..b940f65 100644 --- a/src/Handlers/AbstractHandler.php +++ b/src/Handlers/AbstractHandler.php @@ -1,8 +1,9 @@ */ - protected $exceptionHandlers = []; + protected array $exceptionHandlers = []; /** * An internal array where the key is the exception class and the value is * the handler class that will treat the exception. * - * @var array + * @var array */ - protected $internalExceptionHandlers = [ - Exception::class => Handler::class, + protected array $internalExceptionHandlers = [ + Throwable::class => Handler::class, ModelNotFoundException::class => ModelNotFoundHandler::class, AuthenticationException::class => AuthenticationHandler::class, ValidationException::class => ValidationHandler::class, @@ -51,11 +52,9 @@ abstract class AbstractHandler ]; /** - * Create instance using the Exception to be handled. - * - * @param Exception $e + * Create instance using the Throwable to be handled. */ - public function __construct(Exception $e) + public function __construct(Throwable $e) { $this->exception = $e; } @@ -63,20 +62,16 @@ public function __construct(Exception $e) /** * Handle with an exception according to specific definitions. Returns one * or more errors using the exception from $exceptions attribute. - * - * @return ErrorHandledInterface|ErrorHandledCollectionInterface */ - abstract public function handle(); + abstract public function handle(): ErrorHandledInterface|ErrorHandledCollectionInterface; /** * Get error code. If code is empty from config file based on type. - * - * @param string $type Code type from config file - * @return int */ - public function getCode($type = 'default') + public function getCode(string $type = 'default'): int|string { - if (empty($code = $this->exception->getCode())) { + $code = $this->exception->getCode(); + if (empty($code)) { return config('json-exception-handler.codes.'.$type); } @@ -86,10 +81,9 @@ public function getCode($type = 'default') /** * Return response with handled exception. * - * @return \SMartins\Exceptions\Response\AbstractResponse * @throws \SMartins\Exceptions\Response\InvalidContentException */ - public function handleException() + public function handleException(): AbstractResponse { $handler = $this->getExceptionHandler(); @@ -103,64 +97,55 @@ public function handleException() /** * Validate response from handle method of handler class. * - * @param ErrorHandledInterface|ErrorHandledCollectionInterface - * @return ErrorHandledCollectionInterface - * * @throws \SMartins\Exceptions\Response\InvalidContentException */ - public function validatedHandledException($error) + public function validatedHandledException(ErrorHandledInterface|ErrorHandledCollectionInterface $error): ErrorHandledCollectionInterface { if ($error instanceof ErrorHandledCollectionInterface) { return $error->validatedContent(ErrorHandledInterface::class); - } elseif ($error instanceof ErrorHandledInterface) { - return $error->toCollection()->setStatusCode($error->getStatus()); } - throw new InvalidArgumentException('The errors must be an instance of ['.ErrorHandledInterface::class.'] or ['.ErrorHandledCollectionInterface::class.'].'); + return $error->toCollection()->setStatusCode($error->getStatus()); } /** * Get the class the will handle the Exception from exceptionHandlers attributes. - * - * @return mixed */ - public function getExceptionHandler() + public function getExceptionHandler(): self { $handlers = $this->getConfiguredHandlers(); - $handler = isset($handlers[get_class($this->exception)]) - ? $handlers[get_class($this->exception)] - : $this->getDefaultHandler(); + $handler = $handlers[get_class($this->exception)] ?? $this->getDefaultHandler(); - return new $handler($this->exception); + if (is_string($handler)) { + return new $handler($this->exception); + } + + return $handler; } /** * Get exception handlers from internal and set on App\Exceptions\Handler.php. * - * @return array + * @return array */ - public function getConfiguredHandlers() + public function getConfiguredHandlers(): array { return array_merge($this->internalExceptionHandlers, $this->exceptionHandlers); } /** * Get default pointer using file and line of exception. - * - * @return string */ - public function getDefaultPointer() + public function getDefaultPointer(): string { return ''; } /** * Get default title from exception. - * - * @return string */ - public function getDefaultTitle() + public function getDefaultTitle(): string { return Str::snake(class_basename($this->exception)); } @@ -168,10 +153,8 @@ public function getDefaultTitle() /** * Get default http code. Check if exception has getStatusCode() methods. * If not get from config file. - * - * @return int */ - public function getStatusCode() + public function getStatusCode(): int { if (method_exists($this->exception, 'getStatusCode')) { return $this->exception->getStatusCode(); @@ -182,10 +165,8 @@ public function getStatusCode() /** * The default handler to handle not treated exceptions. - * - * @return \SMartins\Exceptions\Handlers\Handler */ - public function getDefaultHandler() + public function getDefaultHandler(): self { return new Handler($this->exception); } @@ -194,9 +175,9 @@ public function getDefaultHandler() * Get default response handler of the if any response handler was defined * on config file. * - * @return string + * @return class-string */ - public function getDefaultResponseHandler() + public function getDefaultResponseHandler(): string { return JsonApiResponse::class; } @@ -204,11 +185,9 @@ public function getDefaultResponseHandler() /** * Get the response class that will handle the json response. * - * @todo Check if the response_handler on config is an instance of - * \SMartins\Exceptions\Response\AbstractResponse - * @return string + * @return class-string */ - public function getResponseHandler() + public function getResponseHandler(): string { $response = config('json-exception-handler.response_handler'); @@ -218,10 +197,9 @@ public function getResponseHandler() /** * Set exception handlers. * - * @param array $handlers - * @return AbstractHandler + * @param array $handlers */ - public function setExceptionHandlers(array $handlers) + public function setExceptionHandlers(array $handlers): static { $this->exceptionHandlers = $handlers; diff --git a/src/Handlers/AuthenticationHandler.php b/src/Handlers/AuthenticationHandler.php index 1d0d1ac..55be86c 100644 --- a/src/Handlers/AuthenticationHandler.php +++ b/src/Handlers/AuthenticationHandler.php @@ -1,21 +1,34 @@ setStatus(401) - ->setCode($this->getCode('authentication')) + return (new Error)->setStatus('401') + ->setCode((string) $this->getCode('authentication')) ->setSource((new Source())->setPointer($this->getDefaultPointer())) ->setTitle($this->getDefaultTitle()) - ->setDetail(__('exception::exceptions.authentication.detail')); + ->setDetail((string) __('exception::exceptions.authentication.detail')); } } diff --git a/src/Handlers/AuthorizationHandler.php b/src/Handlers/AuthorizationHandler.php index 0ff9b8f..0252be0 100644 --- a/src/Handlers/AuthorizationHandler.php +++ b/src/Handlers/AuthorizationHandler.php @@ -1,21 +1,34 @@ setStatus(403) - ->setCode($this->getCode('authorization')) + return (new Error)->setStatus('403') + ->setCode((string) $this->getCode('authorization')) ->setSource((new Source())->setPointer($this->getDefaultPointer())) - ->setTitle(__('exception::exceptions.authorization.title')) + ->setTitle((string) __('exception::exceptions.authorization.title')) ->setDetail($this->exception->getMessage()); } } diff --git a/src/Handlers/BadRequestHttpHandler.php b/src/Handlers/BadRequestHttpHandler.php index 9a3ffb5..fc70c29 100644 --- a/src/Handlers/BadRequestHttpHandler.php +++ b/src/Handlers/BadRequestHttpHandler.php @@ -1,19 +1,32 @@ setStatus(400) - ->setCode($this->getCode('bad_request')) + return (new Error)->setStatus('400') + ->setCode((string) $this->getCode('bad_request')) ->setSource((new Source())->setPointer($this->getDefaultPointer())) ->setTitle($this->getDefaultTitle()) ->setDetail($this->exception->getMessage()); diff --git a/src/Handlers/Handler.php b/src/Handlers/Handler.php index d9ee00b..2312f43 100644 --- a/src/Handlers/Handler.php +++ b/src/Handlers/Handler.php @@ -1,19 +1,20 @@ setStatus($this->getStatusCode()) - ->setCode($this->getCode()) + return (new Error)->setStatus((string) $this->getStatusCode()) + ->setCode((string) $this->getCode()) ->setSource((new Source())->setPointer($this->getDefaultPointer())) ->setTitle($this->getDefaultTitle()) ->setDetail($this->exception->getMessage()); diff --git a/src/Handlers/MissingScopeHandler.php b/src/Handlers/MissingScopeHandler.php index 8819057..29152b6 100644 --- a/src/Handlers/MissingScopeHandler.php +++ b/src/Handlers/MissingScopeHandler.php @@ -1,19 +1,20 @@ setStatus(403) - ->setCode($this->getCode('missing_scope')) + return (new Error)->setStatus('403') + ->setCode((string) $this->getCode('missing_scope')) ->setSource((new Source())->setPointer($this->getDefaultPointer())) ->setTitle($this->getDefaultTitle()) ->setDetail($this->exception->getMessage()); diff --git a/src/Handlers/ModelNotFoundHandler.php b/src/Handlers/ModelNotFoundHandler.php index 4a24563..6dff20d 100644 --- a/src/Handlers/ModelNotFoundHandler.php +++ b/src/Handlers/ModelNotFoundHandler.php @@ -1,59 +1,60 @@ extractEntityName($this->exception->getModel()); $detail = __('exception::exceptions.model_not_found.title', ['model' => $entity]); - return (new Error)->setStatus(404) - ->setCode($this->getCode('model_not_found')) + return (new Error)->setStatus('404') + ->setCode((string) $this->getCode('model_not_found')) ->setSource((new Source())->setPointer('data/id')) ->setTitle(Str::snake(class_basename($this->exception))) - ->setDetail($detail); + ->setDetail((string) $detail); } /** * Get entity name based on model path to mount the message. - * - * @param string $model - * @return string */ - public function extractEntityName(string $model) + public function extractEntityName(?string $model): string { - $classNames = (array) explode('\\', $model); + if ($model === null) { + return 'Model'; + } + + $classNames = explode('\\', $model); $entityName = end($classNames); if ($this->entityHasTranslation($entityName)) { - return __('exception::exceptions.models.'.$entityName); + return (string) __('exception::exceptions.models.'.$entityName); } return $entityName; @@ -62,28 +63,21 @@ public function extractEntityName(string $model) /** * Check if entity returned on ModelNotFoundException has translation on * exceptions file. - * - * @param string $entityName The model name to check if has translation - * @return bool Has translation or not */ public function entityHasTranslation(string $entityName): bool { - $hasKey = in_array($entityName, $this->translationModelKeys()); - - if ($hasKey) { - return ! empty($hasKey); - } - - return false; + return in_array($entityName, $this->translationModelKeys(), true); } /** * Get the models keys on exceptions lang file. * - * @return array An array with keys to translate + * @return array */ private function translationModelKeys(): array { - return array_keys(__('exception::exceptions.models')); + $models = __('exception::exceptions.models'); + + return is_array($models) ? array_keys($models) : []; } } diff --git a/src/Handlers/NotFoundHttpHandler.php b/src/Handlers/NotFoundHttpHandler.php index 9eae341..504c691 100644 --- a/src/Handlers/NotFoundHttpHandler.php +++ b/src/Handlers/NotFoundHttpHandler.php @@ -1,19 +1,32 @@ setStatus($this->getStatusCode()) - ->setCode($this->getCode('not_found_http')) + return (new Error)->setStatus((string) $this->getStatusCode()) + ->setCode((string) $this->getCode('not_found_http')) ->setSource((new Source())->setPointer($this->getDefaultPointer())) ->setTitle($this->getDefaultTitle()) ->setDetail($this->getNotFoundMessage()); @@ -21,17 +34,15 @@ public function handle() /** * Get message based on file. If file is RouteCollection return specific message. - * - * @return string */ - public function getNotFoundMessage() + public function getNotFoundMessage(): string { $message = ! empty($this->exception->getMessage()) ? $this->exception->getMessage() : class_basename($this->exception); if (basename($this->exception->getFile()) === 'RouteCollection.php') { - $message = __('exception::exceptions.not_found_http.message'); + $message = (string) __('exception::exceptions.not_found_http.message'); } return $message; diff --git a/src/Handlers/OAuthServerHandler.php b/src/Handlers/OAuthServerHandler.php index 9b752f0..95b84c0 100644 --- a/src/Handlers/OAuthServerHandler.php +++ b/src/Handlers/OAuthServerHandler.php @@ -1,30 +1,35 @@ setStatus($this->getHttpStatusCode()) - ->setCode($this->getCode()) + return (new Error)->setStatus((string) $this->getStatusCode()) + ->setCode((string) $this->getCode('oauth_server')) ->setSource((new Source())->setPointer($this->getDefaultPointer())) ->setTitle($this->exception->getErrorType()) ->setDetail($this->exception->getMessage()); diff --git a/src/Handlers/ValidationHandler.php b/src/Handlers/ValidationHandler.php index b22b3b2..90425f2 100644 --- a/src/Handlers/ValidationHandler.php +++ b/src/Handlers/ValidationHandler.php @@ -1,19 +1,32 @@ setStatusCode(422); + parent::__construct($e); + } + + public function handle(): ErrorHandledInterface|ErrorHandledCollectionInterface + { + $errors = (new ErrorCollection)->setStatusCode('422'); $failedFieldsRules = $this->getFailedFieldsRules(); @@ -22,13 +35,13 @@ public function handle() $code = $this->getValidationCode($failedFieldsRules, $key, $field); $title = $this->getValidationTitle($failedFieldsRules, $key, $field); - $error = (new Error)->setStatus(422) + $error = (new Error)->setStatus('422') ->setSource((new Source())->setPointer($field)) ->setTitle($title ?? $this->getDefaultTitle()) ->setDetail($message); if (! is_null($code)) { - $error->setCode($code); + $error->setCode((string) $code); } $errors->push($error); @@ -40,13 +53,8 @@ public function handle() /** * Get the title of response based on rules and field getting from translations. - * - * @param array $failedFieldsRules - * @param string $key - * @param string $field - * @return string|null */ - public function getValidationTitle(array $failedFieldsRules, string $key, string $field) + public function getValidationTitle(array $failedFieldsRules, int $key, string $field): ?string { $title = __('exception::exceptions.validation.title', [ 'fails' => strtolower(array_keys($failedFieldsRules[$field])[$key]), @@ -58,13 +66,8 @@ public function getValidationTitle(array $failedFieldsRules, string $key, string /** * Get the code of validation error from config. - * - * @param array $failedFieldsRules - * @param string $key - * @param string $field - * @return string|null */ - public function getValidationCode(array $failedFieldsRules, string $key, string $field) + public function getValidationCode(array $failedFieldsRules, int $key, string $field): int|string|null { $rule = strtolower(array_keys($failedFieldsRules[$field])[$key]); @@ -77,7 +80,7 @@ public function getValidationCode(array $failedFieldsRules, string $key, string * response object. If exception is generated by Validator::make() the * messages are get different. * - * @return array + * @return array> */ public function getFailedFieldsMessages(): array { @@ -87,7 +90,7 @@ public function getFailedFieldsMessages(): array /** * Get the rules failed on fields. * - * @return array + * @return array>> */ public function getFailedFieldsRules(): array { diff --git a/src/JsonApi/Error.php b/src/JsonApi/Error.php index 7af2385..e81a094 100644 --- a/src/JsonApi/Error.php +++ b/src/JsonApi/Error.php @@ -1,5 +1,7 @@ + */ class Error implements Arrayable, ErrorHandledInterface { use NotNullArrayable; /** * A unique identifier for this particular occurrence of the problem. - * - * @var string */ - protected $id; + protected ?string $id = null; - /** - * @var \SMartins\Exceptions\JsonApi\Links - */ - protected $links; + protected ?Links $links = null; /** * The HTTP status code applicable to this problem, expressed as a string value. - * - * @var string */ - protected $status; + protected ?string $status = null; /** * An application-specific error code, expressed as a string value. - * - * @var string */ - protected $code; + protected ?string $code = null; /** * A short, human-readable summary of the problem that SHOULD NOT change from * occurrence to occurrence of the problem, except for purposes of localization. - * - * @var string */ - protected $title; + protected ?string $title = null; /** * A human-readable explanation specific to this occurrence of the problem. - * Like title, this field’s value can be localized. - * - * @var string + * Like title, this field's value can be localized. */ - protected $detail; + protected ?string $detail = null; /** * An object containing references to the source of the error. - * - * @var \SMartins\Exceptions\JsonApi\Source */ - protected $source; + protected ?Source $source = null; /** * Get a unique identifier for this particular occurrence of the problem. - * - * @return string */ - public function getId() + public function getId(): ?string { return $this->id; } /** * Set a unique identifier for this particular occurrence of the problem. - * - * @param string $id - * - * @return self */ - public function setId(string $id): self + public function setId(string $id): static { $this->id = $id; @@ -86,44 +70,31 @@ public function setId(string $id): self /** * Get the value of links. - * - * @return \SMartins\Exceptions\JsonApi\Links */ - public function getLinks() + public function getLinks(): ?Links { return $this->links; } /** * Set the value of links. - * - * @param \SMartins\Exceptions\JsonApi\Links $links - * - * @return self */ - public function setLinks(Links $links): self + public function setLinks(Links $links): static { $this->links = $links; return $this; } - /** - * {@inheritdoc} - */ public function getStatus(): string { - return $this->status; + return $this->status ?? ''; } /** * Set the HTTP status code applicable to this problem, expressed as a string value. - * - * @param string $status - * - * @return self */ - public function setStatus(string $status): self + public function setStatus(string $status): static { $this->status = $status; @@ -132,22 +103,16 @@ public function setStatus(string $status): self /** * Get an application-specific error code, expressed as a string value. - * - * @return string */ - public function getCode() + public function getCode(): ?string { return $this->code; } /** * Set an application-specific error code, expressed as a string value. - * - * @param string $code - * - * @return self */ - public function setCode(string $code): self + public function setCode(string $code): static { $this->code = $code; @@ -156,22 +121,16 @@ public function setCode(string $code): self /** * Get occurrence to occurrence of the problem, except for purposes of localization. - * - * @return string */ public function getTitle(): string { - return $this->title; + return $this->title ?? ''; } /** * Set occurrence to occurrence of the problem, except for purposes of localization. - * - * @param string $title - * - * @return self */ - public function setTitle(string $title): self + public function setTitle(string $title): static { $this->title = $title; @@ -179,23 +138,17 @@ public function setTitle(string $title): self } /** - * Get like title, this field’s value can be localized. - * - * @return string + * Get like title, this field's value can be localized. */ - public function getDetail() + public function getDetail(): ?string { return $this->detail; } /** - * Set like title, this field’s value can be localized. - * - * @param string $detail - * - * @return self + * Set like title, this field's value can be localized. */ - public function setDetail(string $detail): self + public function setDetail(string $detail): static { $this->detail = $detail; @@ -204,31 +157,22 @@ public function setDetail(string $detail): self /** * Get an object containing references to the source of the error. - * - * @return \SMartins\Exceptions\JsonApi\Source */ - public function getSource() + public function getSource(): ?Source { return $this->source; } /** * Set an object containing references to the source of the error. - * - * @param \SMartins\Exceptions\JsonApi\Source $source - * - * @return self */ - public function setSource(Source $source): self + public function setSource(Source $source): static { $this->source = $source; return $this; } - /** - * {@inheritdoc} - */ public function toCollection(): ErrorHandledCollectionInterface { return new ErrorCollection([$this]); diff --git a/src/JsonApi/ErrorCollection.php b/src/JsonApi/ErrorCollection.php index 8efb248..6ded3d9 100644 --- a/src/JsonApi/ErrorCollection.php +++ b/src/JsonApi/ErrorCollection.php @@ -1,33 +1,34 @@ + */ class ErrorCollection extends Collection implements ErrorHandledCollectionInterface { /** * The HTTP status code applicable to this problem, expressed as a string value. - * - * @var string */ - protected $statusCode; + protected ?string $statusCode = null; /** * The HTTP headers on response. * - * @var array + * @var array */ - protected $headers = []; + protected array $headers = []; /** * Returns the status code. - * - * @return string|null */ - public function getStatusCode() + public function getStatusCode(): ?string { return $this->statusCode; } @@ -35,7 +36,7 @@ public function getStatusCode() /** * Returns response headers. * - * @return array headers + * @return array */ public function getHeaders(): array { @@ -44,12 +45,8 @@ public function getHeaders(): array /** * Set the status code. - * - * @param string $statusCode - * - * @return self */ - public function setStatusCode(string $statusCode) + public function setStatusCode(string $statusCode): static { $this->statusCode = $statusCode; @@ -59,11 +56,9 @@ public function setStatusCode(string $statusCode) /** * Set the headers of response. * - * @param array $headers - * - * @return self + * @param array $headers */ - public function setHeaders(array $headers): self + public function setHeaders(array $headers): static { $this->headers = $headers; @@ -71,7 +66,7 @@ public function setHeaders(array $headers): self } /** - * {@inheritdoc} + * @throws InvalidContentException */ public function validatedContent(string $type): ErrorHandledCollectionInterface { diff --git a/src/JsonApi/Links.php b/src/JsonApi/Links.php index 218c951..0b6b456 100644 --- a/src/JsonApi/Links.php +++ b/src/JsonApi/Links.php @@ -1,18 +1,40 @@ + */ +class Links implements Arrayable { use NotNullArrayable; /** * A link that leads to further details about this particular occurrence of * the problem. - * - * @var string */ - protected $about; + protected ?string $about = null; + + /** + * Get the about link. + */ + public function getAbout(): ?string + { + return $this->about; + } + + /** + * Set the about link. + */ + public function setAbout(string $about): static + { + $this->about = $about; + + return $this; + } } diff --git a/src/JsonApi/Response.php b/src/JsonApi/Response.php index 3e2351d..28cf878 100644 --- a/src/JsonApi/Response.php +++ b/src/JsonApi/Response.php @@ -1,5 +1,7 @@ + */ class Source implements Arrayable { use NotNullArrayable; @@ -13,36 +18,26 @@ class Source implements Arrayable * A JSON Pointer [RFC6901] to the associated entity in the request document * [e.g. "/data" for a primary data object, or "/data/attributes/title" for * a specific attribute]. - * - * @var string */ - protected $pointer; + protected ?string $pointer = null; /** * A string indicating which URI query parameter caused the error. - * - * @var string */ - protected $parameter; + protected ?string $parameter = null; /** * Get pointer. - * - * @return string */ - public function getPointer() + public function getPointer(): ?string { return $this->pointer; } /** * Set pointer. - * - * @param string $pointer - * - * @return self */ - public function setPointer(string $pointer) + public function setPointer(string $pointer): static { $this->pointer = $pointer; @@ -51,22 +46,16 @@ public function setPointer(string $pointer) /** * Get parameter. - * - * @return string */ - public function getParameter() + public function getParameter(): ?string { return $this->parameter; } /** * Set parameter. - * - * @param string $parameter - * - * @return self */ - public function setParameter(string $parameter) + public function setParameter(string $parameter): static { $this->parameter = $parameter; diff --git a/src/JsonHandler.php b/src/JsonHandler.php index af19e7c..3fc4509 100644 --- a/src/JsonHandler.php +++ b/src/JsonHandler.php @@ -1,7 +1,10 @@ publishes([ $this->configPath() => config_path('json-exception-handler.php'), @@ -15,16 +17,16 @@ public function boot() $this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'exception'); $this->publishes([ - __DIR__.'/../resources/lang' => resource_path('lang/vendor/exception'), + __DIR__.'/../resources/lang' => $this->app->langPath('vendor/exception'), ]); } - public function register() + public function register(): void { $this->mergeConfigFrom($this->configPath(), 'json-exception-handler'); } - public function configPath() + public function configPath(): string { return __DIR__.'/../config/json-exception-handler.php'; } diff --git a/src/Response/AbstractResponse.php b/src/Response/AbstractResponse.php index abf2657..320d3cf 100644 --- a/src/Response/AbstractResponse.php +++ b/src/Response/AbstractResponse.php @@ -1,5 +1,7 @@ status; } /** * Set the HTTP status code. - * - * @param int $status The HTTP status code. - * - * @return self */ - public function setStatus(int $status) + public function setStatus(int $status): static { $this->status = $status; @@ -58,18 +48,14 @@ public function setStatus(int $status) /** * Get the errors on response. - * - * @return \SMartins\Exceptions\Response\ErrorHandledCollectionInterface */ - public function getErrors() + public function getErrors(): ErrorHandledCollectionInterface { return $this->errors; } /** * Returns JSON response. - * - * @return \Illuminate\Http\JsonResponse */ abstract public function json(): JsonResponse; } diff --git a/src/Response/ErrorCollectionInterface.php b/src/Response/ErrorCollectionInterface.php index f30861d..bae6dca 100644 --- a/src/Response/ErrorCollectionInterface.php +++ b/src/Response/ErrorCollectionInterface.php @@ -1,31 +1,30 @@ + */ interface ErrorCollectionInterface extends Arrayable { /** * Returns response headers. * - * @return array Response headers + * @return array */ - public function getHeaders(); + public function getHeaders(): array; /** * Set HTTP status code of response. - * - * @param string $statusCode - * - * @return static */ - public function setStatusCode(string $statusCode); + public function setStatusCode(string $statusCode): static; /** * Get the HTTP status code. - * - * @return string|null */ - public function getStatusCode(); + public function getStatusCode(): ?string; } diff --git a/src/Response/ErrorHandledCollectionInterface.php b/src/Response/ErrorHandledCollectionInterface.php index 5541e92..d4dcfb3 100644 --- a/src/Response/ErrorHandledCollectionInterface.php +++ b/src/Response/ErrorHandledCollectionInterface.php @@ -1,5 +1,7 @@ */ - public function toArray() + public function toArray(): array { $array = []; foreach (get_object_vars($this) as $attribute => $value) { diff --git a/tests/Feature/JsonHandlerTest.php b/tests/Feature/JsonHandlerTest.php index 2ded966..f7df050 100644 --- a/tests/Feature/JsonHandlerTest.php +++ b/tests/Feature/JsonHandlerTest.php @@ -1,19 +1,22 @@ setUpRoutes(); } - public function setUpRoutes() + public function setUpRoutes(): void { Route::get('default_exception', function (Request $request) { throw new Exception('Test message', 1); @@ -52,35 +55,40 @@ public function setUpRoutes() }); } - public function testThrowsDefaultException() + #[Test] + public function testThrowsDefaultException(): void { $this->json('GET', 'default_exception') ->assertStatus(500) ->assertJsonStructure($this->defaultErrorStructure()); } - public function testThrowsModelNotFoundException() + #[Test] + public function testThrowsModelNotFoundException(): void { $this->json('GET', 'model_not_found') ->assertStatus(404) ->assertJsonStructure($this->defaultErrorStructure()); } - public function testThrowsAuthenticationException() + #[Test] + public function testThrowsAuthenticationException(): void { $this->json('GET', 'authentication') ->assertStatus(401) ->assertJsonStructure($this->defaultErrorStructure()); } - public function testThrowsAuthorizationException() + #[Test] + public function testThrowsAuthorizationException(): void { $this->json('GET', 'authorization') ->assertStatus(403) ->assertJsonStructure($this->defaultErrorStructure()); } - public function testThrowsValidationExceptions() + #[Test] + public function testThrowsValidationExceptions(): void { $params = ['email' => str_repeat('a', 11)]; @@ -89,14 +97,16 @@ public function testThrowsValidationExceptions() ->assertJsonStructure($this->defaultErrorStructure()); } - public function testThrowsBadRequestHttpException() + #[Test] + public function testThrowsBadRequestHttpException(): void { $this->json('GET', 'bad_request') ->assertStatus(400) ->assertJsonStructure($this->defaultErrorStructure()); } - public function testThrowsNotFoundHttpException() + #[Test] + public function testThrowsNotFoundHttpException(): void { $this->json('GET', 'not_found_route') ->assertStatus(404) @@ -106,9 +116,9 @@ public function testThrowsNotFoundHttpException() /** * The default json response error structure. * - * @return array + * @return array|string>>> */ - public function defaultErrorStructure() + public function defaultErrorStructure(): array { return [ 'errors' => [[ @@ -116,7 +126,7 @@ public function defaultErrorStructure() 'code', 'title', 'detail', - 'source' => ['pointer'] + 'source' => ['pointer'], ]], ]; } diff --git a/tests/Fixtures/Exceptions/Handler.php b/tests/Fixtures/Exceptions/Handler.php index 5b5952e..f643c83 100644 --- a/tests/Fixtures/Exceptions/Handler.php +++ b/tests/Fixtures/Exceptions/Handler.php @@ -1,14 +1,18 @@ */ protected $exceptionHandlers = [ AuthorizationException::class => AuthorizationHandler::class, @@ -28,7 +32,7 @@ class Handler extends ExceptionHandler /** * A list of the exception types that are not reported. * - * @var array + * @var array> */ protected $dontReport = [ // @@ -37,7 +41,7 @@ class Handler extends ExceptionHandler /** * A list of the inputs that are never flashed for validation exceptions. * - * @var array + * @var array */ protected $dontFlash = [ 'password', @@ -46,28 +50,21 @@ class Handler extends ExceptionHandler /** * Report or log an exception. - * - * @param \Exception $exception - * @return void */ - public function report(Exception $exception) + public function report(Throwable $e): void { - parent::report($exception); + parent::report($e); } /** * Render an exception into an HTTP response. - * - * @param \Illuminate\Http\Request $request - * @param \Exception $exception - * @return \Illuminate\Http\Response */ - public function render($request, Exception $exception) + public function render($request, Throwable $e): Response { if ($request->expectsJson()) { - return $this->jsonResponse($exception); + return $this->jsonResponse($e); } - return parent::render($request, $exception); + return parent::render($request, $e); } } diff --git a/tests/Fixtures/User.php b/tests/Fixtures/User.php index 58f5e55..31d2555 100644 --- a/tests/Fixtures/User.php +++ b/tests/Fixtures/User.php @@ -1,10 +1,11 @@ singleton( ExceptionHandler::class, @@ -25,12 +27,16 @@ protected function getEnvironmentSetUp($app) // Setup default database to use sqlite :memory: $app['config']->set('database.default', 'exceptions'); $app['config']->set('database.connections.exceptions', [ - 'driver' => 'sqlite', + 'driver' => 'sqlite', 'database' => ':memory:', ]); } - protected function getPackageProviders($app) + /** + * @param Application $app + * @return array + */ + protected function getPackageProviders($app): array { return [ JsonHandlerServiceProvider::class, diff --git a/tests/Unit/AbstractHandlerTest.php b/tests/Unit/AbstractHandlerTest.php index 67acc67..f5a6344 100644 --- a/tests/Unit/AbstractHandlerTest.php +++ b/tests/Unit/AbstractHandlerTest.php @@ -1,34 +1,40 @@ assertInstanceOf(Handler::class, $handler->getExceptionHandler()); } - public function testGetDefaultHandler() + #[Test] + public function testGetDefaultHandler(): void { - $handler = new Handler(new \Exception); + $handler = new Handler(new Exception); $this->assertInstanceOf(Handler::class, $handler->getDefaultHandler()); } - public function testValidateHandledExceptionWithInvalidArgument() + #[Test] + public function testValidateHandledExceptionWithInvalidArgument(): void { - $this->expectException(InvalidArgumentException::class); + $this->expectException(TypeError::class); - $handler = new Handler(new \Exception); + $handler = new Handler(new Exception); + // @phpstan-ignore-next-line $handler->validatedHandledException('invalid'); } } diff --git a/tests/Unit/ErrorCollectionTest.php b/tests/Unit/ErrorCollectionTest.php index 0c48620..05bc7f2 100644 --- a/tests/Unit/ErrorCollectionTest.php +++ b/tests/Unit/ErrorCollectionTest.php @@ -1,14 +1,17 @@ assertInstanceOf(ErrorCollection::class, $error->setHeaders(['foo' => 'bar'])); diff --git a/tests/Unit/ErrorTest.php b/tests/Unit/ErrorTest.php index 5e5b235..8287a26 100644 --- a/tests/Unit/ErrorTest.php +++ b/tests/Unit/ErrorTest.php @@ -1,59 +1,68 @@ assertInstanceOf(Error::class, $error->setId(1)); - $this->assertEquals(1, $error->getId()); + $this->assertInstanceOf(Error::class, $error->setId('1')); + $this->assertEquals('1', $error->getId()); } - public function testGetAndSetLinks() + #[Test] + public function testGetAndSetLinks(): void { $error = new Error; $this->assertInstanceOf(Error::class, $error->setLinks($links = new Links)); $this->assertEquals($links, $error->getLinks()); } - public function testGetAndSetCode() + #[Test] + public function testGetAndSetCode(): void { $error = new Error; - $this->assertInstanceOf(Error::class, $error->setCode(1)); - $this->assertEquals(1, $error->getCode()); + $this->assertInstanceOf(Error::class, $error->setCode('1')); + $this->assertEquals('1', $error->getCode()); } - public function testGetAndSetTitle() + #[Test] + public function testGetAndSetTitle(): void { $error = new Error; $this->assertInstanceOf(Error::class, $error->setTitle('tests')); $this->assertEquals('tests', $error->getTitle()); } - public function testGetAndSetDetail() + #[Test] + public function testGetAndSetDetail(): void { $error = new Error; $this->assertInstanceOf(Error::class, $error->setDetail('detail')); $this->assertEquals('detail', $error->getDetail()); } - public function testGetAndSerSource() + #[Test] + public function testGetAndSerSource(): void { $error = new Error; $this->assertInstanceOf(Error::class, $error->setSource($source = new Source)); $this->assertEquals($source, $error->getSource()); } - public function testToCollection() + #[Test] + public function testToCollection(): void { $error = new Error; $this->assertInstanceOf(ErrorCollection::class, $error->toCollection()); diff --git a/tests/Unit/HandlerTest.php b/tests/Unit/HandlerTest.php index afe9d91..fa81d73 100644 --- a/tests/Unit/HandlerTest.php +++ b/tests/Unit/HandlerTest.php @@ -1,15 +1,19 @@ setModel(NotTranslated::class); @@ -20,7 +21,7 @@ public function it_will_return_the_model_class_name_without_translations() $error = $handler->handle(); - $this->assertEquals(404, $error->getStatus()); + $this->assertEquals('404', $error->getStatus()); $this->assertEquals( 'NotTranslated not found', $error->getDetail() @@ -28,4 +29,6 @@ public function it_will_return_the_model_class_name_without_translations() } } -class NotTranslated extends Model {} +class NotTranslated extends Model +{ +} diff --git a/tests/Unit/SourceTest.php b/tests/Unit/SourceTest.php index adf89e8..ec20cea 100644 --- a/tests/Unit/SourceTest.php +++ b/tests/Unit/SourceTest.php @@ -1,13 +1,19 @@