diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cd83ec5e1..6c3896ee4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,10 +10,10 @@ jobs: strategy: fail-fast: false matrix: - php: [7.4, 8.0, 8.1, 8.2] + php: [8.2, 8.3, 8.4] experimental: [false] include: - - php: 8.1 + - php: 8.3 analysis: true steps: diff --git a/README.md b/README.md index 63f997258..09681f188 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This skeleton application was built for Composer. This makes setting up a new Sl ## Install the Application -Run this command from the directory in which you want to install your new Slim Framework application. You will require PHP 7.4 or newer. +Run this command from the directory in which you want to install your new Slim Framework application. You will require PHP 8.3 or newer. ```bash composer create-project slim/slim-skeleton [my-app-name] diff --git a/app/settings.php b/app/settings.php index 79392bd8d..f823aafa5 100644 --- a/app/settings.php +++ b/app/settings.php @@ -5,6 +5,7 @@ use App\Application\Settings\Settings; use App\Application\Settings\SettingsInterface; use DI\ContainerBuilder; +use Monolog\Level; use Monolog\Logger; return function (ContainerBuilder $containerBuilder) { @@ -19,7 +20,7 @@ 'logger' => [ 'name' => 'slim-app', 'path' => isset($_ENV['docker']) ? 'php://stdout' : __DIR__ . '/../logs/app.log', - 'level' => Logger::DEBUG, + 'level' => Level::fromName('DEBUG'), ], ]); } diff --git a/composer.json b/composer.json index b74219b9a..cfb6f9010 100644 --- a/composer.json +++ b/composer.json @@ -22,20 +22,20 @@ } ], "require": { - "php": "^7.4 || ^8.0", + "php": "^8.2.0 || ^8.3.0 || ^8.4.0", "ext-json": "*", - "monolog/monolog": "^2.9", - "php-di/php-di": "^6.4", + "monolog/monolog": "^3.8", + "php-di/php-di": "^7.0", "slim/psr7": "^1.6", "slim/slim": "^4.12" }, "require-dev": { - "jangregor/phpstan-prophecy": "^1.0.0", - "phpspec/prophecy-phpunit": "^2.2", + "jangregor/phpstan-prophecy": "^2.0", + "phpspec/prophecy-phpunit": "^2.3", "phpstan/extension-installer": "^1.3.1", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.6.17", - "squizlabs/php_codesniffer": "^3.9" + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^11.5", + "squizlabs/php_codesniffer": "^3.11" }, "config": { "allow-plugins": { diff --git a/docker-compose.yml b/docker-compose.yml index f8976b7fd..fa89ed71b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ volumes: services: slim: - image: php:8-alpine + image: php:8.4-alpine working_dir: /var/www command: php -S 0.0.0.0:8080 -t public environment: diff --git a/phpunit.xml b/phpunit.xml index f83040094..b7d0e7286 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,27 +1,13 @@ - - - - ./tests/ - - - - - ./src/ - - + + + + + ./tests/ + + + + + ./src/ + + diff --git a/public/index.php b/public/index.php index bc583c821..75a373228 100644 --- a/public/index.php +++ b/public/index.php @@ -16,7 +16,7 @@ $containerBuilder = new ContainerBuilder(); if (false) { // Should be set to true in production - $containerBuilder->enableCompilation(__DIR__ . '/../var/cache'); + $containerBuilder->enableCompilation(__DIR__ . '/../var/cache'); } // Set up settings diff --git a/src/Application/Actions/Action.php b/src/Application/Actions/Action.php index 1021c2023..bd146cbb2 100644 --- a/src/Application/Actions/Action.php +++ b/src/Application/Actions/Action.php @@ -13,17 +13,17 @@ abstract class Action { - protected LoggerInterface $logger; - protected Request $request; protected Response $response; + /** + * @var array + */ protected array $args; - public function __construct(LoggerInterface $logger) + public function __construct(protected LoggerInterface $logger) { - $this->logger = $logger; } /** @@ -49,10 +49,7 @@ public function __invoke(Request $request, Response $response, array $args): Res */ abstract protected function action(): Response; - /** - * @return array|object - */ - protected function getFormData() + protected function getFormData(): array|object|null { return $this->request->getParsedBody(); } @@ -70,10 +67,7 @@ protected function resolveArg(string $name) return $this->args[$name]; } - /** - * @param array|object|null $data - */ - protected function respondWithData($data = null, int $statusCode = 200): Response + protected function respondWithData(array|object|null $data = null, int $statusCode = 200): Response { $payload = new ActionPayload($statusCode, $data); @@ -87,6 +81,6 @@ protected function respond(ActionPayload $payload): Response return $this->response ->withHeader('Content-Type', 'application/json') - ->withStatus($payload->getStatusCode()); + ->withStatus($payload->statusCode); } } diff --git a/src/Application/Actions/ActionError.php b/src/Application/Actions/ActionError.php index 3a81cc53f..8b36f36cd 100644 --- a/src/Application/Actions/ActionError.php +++ b/src/Application/Actions/ActionError.php @@ -18,36 +18,10 @@ class ActionError implements JsonSerializable public const VALIDATION_ERROR = 'VALIDATION_ERROR'; public const VERIFICATION_ERROR = 'VERIFICATION_ERROR'; - private string $type; - - private ?string $description; - - public function __construct(string $type, ?string $description = null) - { - $this->type = $type; - $this->description = $description; - } - - public function getType(): string - { - return $this->type; - } - - public function setType(string $type): self - { - $this->type = $type; - return $this; - } - - public function getDescription(): ?string - { - return $this->description; - } - - public function setDescription(?string $description = null): self - { - $this->description = $description; - return $this; + public function __construct( + public readonly string $type, + public readonly ?string $description = null + ) { } #[\ReturnTypeWillChange] diff --git a/src/Application/Actions/ActionPayload.php b/src/Application/Actions/ActionPayload.php index cbf511040..6137e4a2a 100644 --- a/src/Application/Actions/ActionPayload.php +++ b/src/Application/Actions/ActionPayload.php @@ -8,41 +8,11 @@ class ActionPayload implements JsonSerializable { - private int $statusCode; - - /** - * @var array|object|null - */ - private $data; - - private ?ActionError $error; - public function __construct( - int $statusCode = 200, - $data = null, - ?ActionError $error = null + public readonly int $statusCode = 200, + public readonly array|object|null $data = null, + public ?ActionError $error = null ) { - $this->statusCode = $statusCode; - $this->data = $data; - $this->error = $error; - } - - public function getStatusCode(): int - { - return $this->statusCode; - } - - /** - * @return array|null|object - */ - public function getData() - { - return $this->data; - } - - public function getError(): ?ActionError - { - return $this->error; } #[\ReturnTypeWillChange] diff --git a/src/Application/Actions/User/UserAction.php b/src/Application/Actions/User/UserAction.php index 4ea2d8caf..dbc7ae8ff 100644 --- a/src/Application/Actions/User/UserAction.php +++ b/src/Application/Actions/User/UserAction.php @@ -10,11 +10,10 @@ abstract class UserAction extends Action { - protected UserRepository $userRepository; - - public function __construct(LoggerInterface $logger, UserRepository $userRepository) - { + public function __construct( + LoggerInterface $logger, + protected UserRepository $userRepository + ) { parent::__construct($logger); - $this->userRepository = $userRepository; } } diff --git a/src/Application/Handlers/HttpErrorHandler.php b/src/Application/Handlers/HttpErrorHandler.php index 64e02b1e3..99f60515b 100644 --- a/src/Application/Handlers/HttpErrorHandler.php +++ b/src/Application/Handlers/HttpErrorHandler.php @@ -26,38 +26,36 @@ protected function respond(): Response { $exception = $this->exception; $statusCode = 500; - $error = new ActionError( - ActionError::SERVER_ERROR, - 'An internal error has occurred while processing your request.' - ); + $errorType = ActionError::SERVER_ERROR; + $description = 'An internal error has occurred while processing your request.'; if ($exception instanceof HttpException) { $statusCode = $exception->getCode(); - $error->setDescription($exception->getMessage()); + $description = $exception->getMessage(); if ($exception instanceof HttpNotFoundException) { - $error->setType(ActionError::RESOURCE_NOT_FOUND); + $errorType = ActionError::RESOURCE_NOT_FOUND; } elseif ($exception instanceof HttpMethodNotAllowedException) { - $error->setType(ActionError::NOT_ALLOWED); + $errorType = ActionError::NOT_ALLOWED; } elseif ($exception instanceof HttpUnauthorizedException) { - $error->setType(ActionError::UNAUTHENTICATED); + $errorType = ActionError::UNAUTHENTICATED; } elseif ($exception instanceof HttpForbiddenException) { - $error->setType(ActionError::INSUFFICIENT_PRIVILEGES); + $errorType = ActionError::INSUFFICIENT_PRIVILEGES; } elseif ($exception instanceof HttpBadRequestException) { - $error->setType(ActionError::BAD_REQUEST); + $errorType = ActionError::BAD_REQUEST; } elseif ($exception instanceof HttpNotImplementedException) { - $error->setType(ActionError::NOT_IMPLEMENTED); + $errorType = ActionError::NOT_IMPLEMENTED; } } if ( !($exception instanceof HttpException) - && $exception instanceof Throwable && $this->displayErrorDetails ) { - $error->setDescription($exception->getMessage()); + $description = $exception->getMessage(); } + $error = new ActionError($errorType, $description); $payload = new ActionPayload($statusCode, null, $error); $encodedPayload = json_encode($payload, JSON_PRETTY_PRINT); diff --git a/src/Application/Handlers/ShutdownHandler.php b/src/Application/Handlers/ShutdownHandler.php index 72b6d46ea..0e173028d 100644 --- a/src/Application/Handlers/ShutdownHandler.php +++ b/src/Application/Handlers/ShutdownHandler.php @@ -10,20 +10,11 @@ class ShutdownHandler { - private Request $request; - - private HttpErrorHandler $errorHandler; - - private bool $displayErrorDetails; - public function __construct( - Request $request, - HttpErrorHandler $errorHandler, - bool $displayErrorDetails + private Request $request, + private HttpErrorHandler $errorHandler, + private bool $displayErrorDetails ) { - $this->request = $request; - $this->errorHandler = $errorHandler; - $this->displayErrorDetails = $displayErrorDetails; } public function __invoke() @@ -47,29 +38,24 @@ public function __invoke() $responseEmitter->emit($response); } - private function getErrorMessage(array $error): string + /** + * @param array{type: int, message: string, file: string, line: int}|null$error + */ + private function getErrorMessage(?array $error = null): string { if (!$this->displayErrorDetails) { return 'An error while processing your request. Please try again later.'; } - $errorFile = $error['file']; - $errorLine = $error['line']; - $errorMessage = $error['message']; - $errorType = $error['type']; - - if ($errorType === E_USER_ERROR) { - return "FATAL ERROR: {$errorMessage}. on line {$errorLine} in file {$errorFile}."; - } - - if ($errorType === E_USER_WARNING) { - return "WARNING: {$errorMessage}"; - } - - if ($errorType === E_USER_NOTICE) { - return "NOTICE: {$errorMessage}"; - } + $errorFile = $error['file'] ?? null; + $errorLine = $error['line'] ?? null; + $errorMessage = $error['message'] ?? null; + $errorType = $error['type'] ?? null; - return "FATAL ERROR: {$errorMessage}. on line {$errorLine} in file {$errorFile}."; + return match ($errorType) { + E_USER_WARNING => "WARNING: {$errorMessage}", + E_USER_NOTICE => "NOTICE: {$errorMessage}", + default => "FATAL ERROR: {$errorMessage}. on line {$errorLine} in file {$errorFile}." + }; } } diff --git a/src/Application/Settings/Settings.php b/src/Application/Settings/Settings.php index 6da55e4e8..40017532f 100644 --- a/src/Application/Settings/Settings.php +++ b/src/Application/Settings/Settings.php @@ -6,18 +6,16 @@ class Settings implements SettingsInterface { - private array $settings; - - public function __construct(array $settings) + /** + * @param array $settings + */ + public function __construct(private array $settings) { $this->settings = $settings; } - /** - * @return mixed - */ - public function get(string $key = '') + public function get(string $key = ''): mixed { - return (empty($key)) ? $this->settings : $this->settings[$key]; + return empty($key) ? $this->settings : $this->settings[$key]; } } diff --git a/src/Application/Settings/SettingsInterface.php b/src/Application/Settings/SettingsInterface.php index 557d98b8d..7867c5920 100644 --- a/src/Application/Settings/SettingsInterface.php +++ b/src/Application/Settings/SettingsInterface.php @@ -6,9 +6,5 @@ interface SettingsInterface { - /** - * @param string $key - * @return mixed - */ - public function get(string $key = ''); + public function get(string $key = ''): mixed; } diff --git a/src/Domain/User/User.php b/src/Domain/User/User.php index 217bb9ebf..e01e8366c 100644 --- a/src/Domain/User/User.php +++ b/src/Domain/User/User.php @@ -8,17 +8,18 @@ class User implements JsonSerializable { - private ?int $id; - private string $username; private string $firstName; private string $lastName; - public function __construct(?int $id, string $username, string $firstName, string $lastName) - { - $this->id = $id; + public function __construct( + private int|null $id, + string $username, + string $firstName, + string $lastName + ) { $this->username = strtolower($username); $this->firstName = ucfirst($firstName); $this->lastName = ucfirst($lastName); diff --git a/src/Infrastructure/Persistence/User/InMemoryUserRepository.php b/src/Infrastructure/Persistence/User/InMemoryUserRepository.php index b85a74fa3..06d2d0fc7 100644 --- a/src/Infrastructure/Persistence/User/InMemoryUserRepository.php +++ b/src/Infrastructure/Persistence/User/InMemoryUserRepository.php @@ -18,7 +18,7 @@ class InMemoryUserRepository implements UserRepository /** * @param User[]|null $users */ - public function __construct(array $users = null) + public function __construct(?array $users = null) { $this->users = $users ?? [ 1 => new User(1, 'bill.gates', 'Bill', 'Gates'), diff --git a/tests/Domain/User/UserTest.php b/tests/Domain/User/UserTest.php index 36f49259f..f7eae5eb9 100644 --- a/tests/Domain/User/UserTest.php +++ b/tests/Domain/User/UserTest.php @@ -5,11 +5,12 @@ namespace Tests\Domain\User; use App\Domain\User\User; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\TestCase; class UserTest extends TestCase { - public function userProvider(): array + public static function userProvider(): array { return [ [1, 'bill.gates', 'Bill', 'Gates'], @@ -20,13 +21,7 @@ public function userProvider(): array ]; } - /** - * @dataProvider userProvider - * @param int $id - * @param string $username - * @param string $firstName - * @param string $lastName - */ + #[DataProvider('userProvider')] public function testGetters(int $id, string $username, string $firstName, string $lastName) { $user = new User($id, $username, $firstName, $lastName); @@ -37,13 +32,7 @@ public function testGetters(int $id, string $username, string $firstName, string $this->assertEquals($lastName, $user->getLastName()); } - /** - * @dataProvider userProvider - * @param int $id - * @param string $username - * @param string $firstName - * @param string $lastName - */ + #[DataProvider('userProvider')] public function testJsonSerialize(int $id, string $username, string $firstName, string $lastName) { $user = new User($id, $username, $firstName, $lastName); diff --git a/tests/bootstrap.php b/tests/bootstrap.php deleted file mode 100644 index d21c14df8..000000000 --- a/tests/bootstrap.php +++ /dev/null @@ -1,3 +0,0 @@ -