From 815dd9f54bd92f7a4d320de1177b7a5738bdf206 Mon Sep 17 00:00:00 2001 From: velkuns Date: Tue, 6 Jan 2026 16:58:56 +0100 Subject: [PATCH 1/3] Component Kernel: PHP 8.3+ support - Add PHP 8.3+ support - Add readonly + use promoted properties on lot of classes - Now defaults & requirements on route are passed to the controller action as arguments - Remove lot useless phpdoc - Update Makefile - Update CI configs - Fix some phpstan errors - Update README to add examples configs and more info - Some phpdoc improvements - Improve Response Time middleware - Improve some tests - Remove some utilities methods - Remove DataCollection class --- .php-cs-fixer.dist.php | 4 +- CHANGELOG.md | 21 ++ Makefile | 71 ++-- README.md | 352 +++++++++++++++++- ci/composer-dependency-analyser.php | 15 + ci/composer-require-checker.json | 5 - ...ibility.neon => phpmax-compatibility.neon} | 2 +- ...ibility.neon => phpmin-compatibility.neon} | 0 composer.json | 36 +- config/kernel.yaml | 10 +- config/packages/application.yaml | 31 ++ config/packages/components.yaml | 31 +- config/packages/middlewares.yaml | 23 ++ config/packages/router.yaml | 14 +- config/routes.yaml | 12 + config/services.yaml | 77 ++-- config/test/services.yaml | 5 + phpstan.neon.dist | 18 +- phpunit.xml.dist | 33 +- phpunit.xml.dist.bak | 35 -- src/Application/Application.php | 96 +---- src/Application/ApplicationInterface.php | 2 +- src/Controller/Controller.php | 84 +---- src/Controller/ControllerInterface.php | 12 - src/Controller/ErrorController.php | 71 +--- src/Controller/ErrorControllerInterface.php | 12 +- src/Exception/HttpBadRequestException.php | 5 - src/Exception/HttpConflictException.php | 5 - src/Exception/HttpForbiddenException.php | 5 - .../HttpInternalServerErrorException.php | 5 - .../HttpMethodNotAllowedException.php | 5 - src/Exception/HttpNotFoundException.php | 5 - .../HttpServiceUnavailableException.php | 5 - .../HttpTooManyRequestsException.php | 5 - src/Exception/HttpUnauthorizedException.php | 5 - src/Kernel.php | 42 +-- src/Middleware/ControllerMiddleware.php | 72 ++-- src/Middleware/ErrorMiddleware.php | 41 +- src/Middleware/RateLimiterMiddleware.php | 42 +-- .../ResponseTimeLoggerMiddleware.php | 86 +++-- src/Middleware/RouterMiddleware.php | 42 +-- src/RateLimiter/Counter/CacheCounter.php | 37 +- src/RateLimiter/Counter/CounterInterface.php | 17 - .../Exception/QuotaExceededException.php | 5 - src/RateLimiter/Limiter/LimiterInterface.php | 6 - src/RateLimiter/Limiter/QuotaLimiter.php | 24 +- .../AbstractQuotaLimiterProvider.php | 28 +- .../RouteQuotaLimiterProvider.php | 20 +- src/Service/DataCollection.php | 149 -------- src/Service/IpResolver.php | 23 +- src/Traits/HttpFactoryAwareTrait.php | 35 -- src/Traits/LoggerAwareTrait.php | 13 - src/Traits/RouterAwareTrait.php | 23 +- src/Traits/ServerRequestAwareTrait.php | 89 +---- tests/Integration/.gitkeep | 0 tests/{unit => Unit}/ApplicationTest.php | 98 +++-- tests/{unit => Unit}/ControllerTest.php | 20 +- tests/{unit => Unit}/KernelTest.php | 16 +- tests/{unit => Unit}/Mock/TestController.php | 166 ++------- .../RateLimiter/CacheCounterTest.php | 38 +- .../RateLimiter/LimiterProviderTest.php | 32 +- .../{unit => Unit}/Service/IpResolverTest.php | 38 +- tests/unit/Service/DataCollectionTest.php | 52 --- 63 files changed, 921 insertions(+), 1450 deletions(-) create mode 100644 ci/composer-dependency-analyser.php delete mode 100644 ci/composer-require-checker.json rename ci/{php81-compatibility.neon => phpmax-compatibility.neon} (92%) rename ci/{php83-compatibility.neon => phpmin-compatibility.neon} (100%) create mode 100644 config/packages/application.yaml create mode 100644 config/packages/middlewares.yaml create mode 100644 config/test/services.yaml delete mode 100644 phpunit.xml.dist.bak delete mode 100644 src/Service/DataCollection.php create mode 100644 tests/Integration/.gitkeep rename tests/{unit => Unit}/ApplicationTest.php (71%) rename tests/{unit => Unit}/ControllerTest.php (90%) rename tests/{unit => Unit}/KernelTest.php (67%) rename tests/{unit => Unit}/Mock/TestController.php (56%) rename tests/{unit => Unit}/RateLimiter/CacheCounterTest.php (68%) rename tests/{unit => Unit}/RateLimiter/LimiterProviderTest.php (83%) rename tests/{unit => Unit}/Service/IpResolverTest.php (69%) delete mode 100644 tests/unit/Service/DataCollectionTest.php diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 87d2ce9..c36d041 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -4,8 +4,8 @@ //~ Rules ->setRules( [ - '@PER-CS2.0' => true, - ] + '@PER-CS3x0' => true, + ], ) //~ Format diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ffc669..72e7967 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [7.0.0] - 2026-01-06 +[7.0.0]: https://github.com/eureka-framework/kernel-http/compare/6.0.0...7.0.0 +### Added +- Add PHP 8.3+ support +- Add readonly + use promoted properties on lot of classes +- Now defaults & requirements on route are passed to the controller action as arguments +### Changed +- Remove lot useless phpdoc +- Update Makefile +- Update CI configs +- Fix some phpstan errors +- Update README to add examples configs and more info +- Some phpdoc improvements +- Improve Response Time middleware +- Improve some tests +### Removed +- Remove some utilities methods +- Remove DataCollection class + +--- + ## [6.0.0] - 2024-02-22 [6.0.0]: https://github.com/eureka-framework/kernel-http/compare/5.3.0...6.0.0 ### Added diff --git a/Makefile b/Makefile index 600acd2..8227c3c 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,8 @@ -.PHONY: validate install update deps phpcs phpcsf php81compatibility php83compatibility phpstan analyze tests testdox ci clean build/reports/phpunit build/reports/phpcs build/reports/phpstan +.PHONY: validate install update php/deps php/check php/fix php/min-compatibility php/max-compatibility php/phpstan php/analyze php/tests php/test php/testdox ci clean +PHP_MIN_VERSION := "8.3" +PHP_MAX_VERSION := "8.5" +COMPOSER_BIN := composer define header = @if [ -t 1 ]; then printf "\n\e[37m\e[100m \e[104m $(1) \e[0m\n"; else printf "\n### $(1)\n"; fi endef @@ -7,77 +10,71 @@ endef #~ Composer dependency validate: $(call header,Composer Validation) - @composer validate + @${COMPOSER_BIN} validate install: $(call header,Composer Install) - @composer install + @${COMPOSER_BIN} install update: $(call header,Composer Update) - @composer update - @composer bump --dev-only + @${COMPOSER_BIN} update + @${COMPOSER_BIN} bump --dev-only composer.lock: install #~ Vendor binaries dependencies -vendor/bin/php-cs-fixer: -vendor/bin/phpstan: -vendor/bin/phpunit: +vendor/bin/php-cs-fixer: composer.lock +vendor/bin/phpstan: composer.lock +vendor/bin/phpunit: composer.lock #~ Report directories dependencies build/reports/phpunit: @mkdir -p build/reports/phpunit -build/reports/phpcs: - @mkdir -p build/reports/cs - build/reports/phpstan: @mkdir -p build/reports/phpstan #~ main commands -deps: composer.json +php/deps: composer.json $(call header,Checking Dependencies) - @XDEBUG_MODE=off ./vendor/bin/composer-require-checker check --config-file="./ci/composer-require-checker.json" + @XDEBUG_MODE=off ./vendor/bin/composer-dependency-analyser --config ./ci/composer-dependency-analyser.php # for shadow, unused required dependencies and ext-* missing dependencies -phpcs: vendor/bin/php-cs-fixer build/reports/phpcs +php/check: vendor/bin/php-cs-fixer $(call header,Checking Code Style) - @./vendor/bin/php-cs-fixer check - -phpcsf: vendor/bin/php-cs-fixer + @XDEBUG_MODE=off ./vendor/bin/php-cs-fixer check +php/fix: vendor/bin/php-cs-fixer $(call header,Fixing Code Style) - @./vendor/bin/php-cs-fixer fix -v + @XDEBUG_MODE=off ./vendor/bin/php-cs-fixer fix -v -php81compatibility: vendor/bin/phpstan build/reports/phpstan - $(call header,Checking PHP 8.1 compatibility) - @XDEBUG_MODE=off ./vendor/bin/phpstan analyse --configuration=./ci/php81-compatibility.neon --error-format=table +php/min-compatibility: vendor/bin/phpstan build/reports/phpstan + $(call header,Checking PHP ${PHP_MIN_VERSION} compatibility) + @XDEBUG_MODE=off ./vendor/bin/phpstan analyse --configuration=./ci/phpmin-compatibility.neon --error-format=table -php83compatibility: vendor/bin/phpstan build/reports/phpstan - $(call header,Checking PHP 8.3 compatibility) - @XDEBUG_MODE=off ./vendor/bin/phpstan analyse --configuration=./ci/php83-compatibility.neon --error-format=table +php/max-compatibility: vendor/bin/phpstan build/reports/phpstan #ci + $(call header,Checking PHP ${PHP_MAX_VERSION} compatibility) + @XDEBUG_MODE=off ./vendor/bin/phpstan analyse --configuration=./ci/phpmax-compatibility.neon --error-format=table -phpstan: vendor/bin/phpstan build/reports/phpstan - $(call header,Running Static Analyze) - @XDEBUG_MODE=off ./vendor/bin/phpstan analyse --error-format=checkstyle > ./build/reports/phpstan/phpstan.xml - -analyze: vendor/bin/phpstan build/reports/phpstan +php/analyze: vendor/bin/phpstan build/reports/phpstan #manual & ci $(call header,Running Static Analyze - Pretty tty format) @XDEBUG_MODE=off ./vendor/bin/phpstan analyse --error-format=table -tests: vendor/bin/phpunit build/reports/phpunit #ci +php/tests: vendor/bin/phpunit build/reports/phpunit #ci $(call header,Running Unit Tests) - @XDEBUG_MODE=coverage php -dzend_extension=xdebug.so ./vendor/bin/phpunit --testsuite=unit --coverage-clover=./build/reports/phpunit/clover.xml --log-junit=./build/reports/phpunit/unit.xml --coverage-php=./build/reports/phpunit/unit.cov --coverage-html=./build/reports/coverage/ --fail-on-warning + @XDEBUG_MODE=coverage php ./vendor/bin/phpunit --testsuite=unit --coverage-clover=./build/reports/phpunit/clover.xml --log-junit=./build/reports/phpunit/unit.xml --coverage-php=./build/reports/phpunit/unit.cov --coverage-html=./build/reports/coverage/ --fail-on-warning + +php/test: php/tests -integration: vendor/bin/phpunit build/reports/phpunit #manual +php/integration: vendor/bin/phpunit build/reports/phpunit #manual $(call header,Running Integration Tests) - @XDEBUG_MODE=coverage php -dzend_extension=xdebug.so ./vendor/bin/phpunit --testsuite=integration --fail-on-warning + @XDEBUG_MODE=coverage php ./vendor/bin/phpunit --testsuite=integration --fail-on-warning -testdox: vendor/bin/phpunit #manual +php/testdox: vendor/bin/phpunit #manual $(call header,Running Unit Tests (Pretty format)) - @XDEBUG_MODE=coverage php -dzend_extension=xdebug.so ./vendor/bin/phpunit --testsuite=unit --fail-on-warning --testdox + @XDEBUG_MODE=coverage php ./vendor/bin/phpunit --testsuite=unit --fail-on-warning --testdox clean: - $(call header,Cleaning previous build) + $(call header,Cleaning previous build) #manual @if [ "$(shell ls -A ./build)" ]; then rm -rf ./build/*; fi; echo " done" -ci: clean validate deps install phpcs tests integration php81compatibility php83compatibility analyze +ci: clean validate install php/deps php/check php/tests php/integration php/min-compatibility php/max-compatibility php/analyze diff --git a/README.md b/README.md index 554b9be..f25dde7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # kernel-http [![Current version](https://img.shields.io/packagist/v/eureka/kernel-http.svg?logo=composer)](https://packagist.org/packages/eureka/kernel-http) -[![Supported PHP version](https://img.shields.io/static/v1?logo=php&label=PHP&message=7.4%20-%208.3&color=777bb4)](https://packagist.org/packages/eureka/kernel-http) +[![Supported PHP version](https://img.shields.io/static/v1?logo=php&label=PHP&message=>%3D8.3&color=777bb4)](https://packagist.org/packages/eureka/kernel-http) ![Build](https://github.com/eureka-framework/kernel-http/workflows/CI/badge.svg) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=eureka-framework_kernel-http&metric=alert_status)](https://sonarcloud.io/dashboard?id=eureka-framework_kernel-http) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=eureka-framework_kernel-http&metric=coverage)](https://sonarcloud.io/dashboard?id=eureka-framework_kernel-http) @@ -57,6 +57,330 @@ try { ``` +### Config tree example +``` +config/ +|_ packages/ +| |_ domains/ +| - user.yaml +| - app.yaml +| - database.yaml +| - services.yaml +|_ dev/ +| - app.yaml +|_ test/ +| - app.yaml +|_ secrets/ +| - database.yaml +|_ services.yaml +``` + +Config loading order and overrides: +1. `kernel.yaml` is loaded +2. Then `services.yaml` +3. Then `packages/` +4. Then `{current_env}/` +5. Finally `secrets/` + + + + +### Kernel config +```yaml +parameters: + + #~ Debug + kernel.debug: false + + #~ Environment + kernel.environment: 'prod' # Overridden in Kernel class, but define here as helper + + #~ Errors + kernel.error.display: 'false' + kernel.error.reporting: !php/const E_ALL + + #~ Directories + kernel.directory.root: '..' # Overridden in Kernel class, but define here as helper + kernel.directory.cache: '%kernel.directory.root%/var/cache' + kernel.directory.log: '%kernel.directory.root%/var/log' + kernel.directory.config: '%kernel.directory.root%/config' + kernel.directory.vendor: '%kernel.directory.root%/vendor' + + #~ Compiler passes + kernel.compiler_pass: + - My\Custom\CompilerPass +``` + + +### Global services config +```yaml +parameters: + app.name: 'kernel' + +services: + # Main container + Psr\Container\ContainerInterface: + alias: 'service_container' + + # Main Logger + logger: + alias: Psr\Log\NullLogger + + # Main cache item pool + cache: + alias: Psr\Cache\CacheItemPoolInterface + + #~ PSR-17 Factories public alias (used by Application via container directly) + response_factory: + alias: Nyholm\Psr7\Factory\Psr17Factory + public: true + + request_factory: + alias: Nyholm\Psr7\Factory\Psr17Factory + public: true + + server_request_factory: + alias: Nyholm\Psr7\Factory\Psr17Factory + public: true + + stream_factory: + alias: Nyholm\Psr7\Factory\Psr17Factory + public: true + + uri_factory: + alias: Nyholm\Psr7\Factory\Psr17Factory + public: true + +``` + +### Application (controllers) config +```yaml +services: + _defaults: + autowire: true + public: true # Important, because Controller middleware use container to get appropriate controller and use it + + #~ Auto discovery kernel controllers (mainly error controller + interfaces) + Eureka\Kernel\Http\Controller\: + resource: '../../vendor/eureka/kernel-http/src/Controller/*' + calls: + - setLogger: [ '@logger' ] + - setRouter: [ '@router' ] + - setResponseFactory: [ '@response_factory' ] + - setRequestFactory: [ '@request_factory' ] + - setServerRequestFactory: [ '@server_request_factory' ] + - setStreamFactory: [ '@stream_factory' ] + - setUriFactory: [ '@uri_factory' ] + + #~ Auto discovery application controllers + Application\Controller\: + resource: '../../src/Controller/*' + calls: + - setLogger: [ '@logger' ] + - setRouter: [ '@router' ] + - setResponseFactory: [ '@response_factory' ] + - setRequestFactory: [ '@request_factory' ] + - setServerRequestFactory: [ '@server_request_factory' ] + - setStreamFactory: [ '@stream_factory' ] + - setUriFactory: [ '@uri_factory' ] + + #~ Auto discovery kernel services + Eureka\Kernel\Http\Service\: + resource: '../../vendor/eureka/kernel-http/src/Service/*' +``` + +### Middleware config +```yaml +parameters: + + # app.middleware Name is important, used by app to get list of middlewares + # order is important, as the first is process, then the following, etc. + app.middleware: + error: 'Eureka\Kernel\Http\Middleware\ErrorMiddleware' + router: 'Eureka\Kernel\Http\Middleware\RouterMiddleware' + logger: 'Eureka\Kernel\Http\Middleware\ResponseTimeLoggerMiddleware' + quota: 'Eureka\Kernel\Http\Middleware\RateLimiterMiddleware' + controller: 'Eureka\Kernel\Http\Middleware\ControllerMiddleware' + +services: + _defaults: + autowire: true + public: true # Must be true, because Application use container to retrieve each middleware and assign them to handler + bind: + $applicationName: '%app.name%' + $logger: '@logger' + $cache: '@cache' + + #~ Global Kernel Services Middleware + Eureka\Kernel\Http\Middleware\: + resource: '../../vendor/eureka/kernel-http/src/Middleware' + + #~ Global Application Services Middleware + Application\Middleware\: + resource: '../../src/Middleware' +``` + +### Router config +```yaml +services: + _defaults: + autowire: true + + router: + alias: Symfony\Component\Routing\Router + + Symfony\Component\Routing\: + resource: '../../vendor/symfony/routing/*' + exclude: '../../vendor/symfony/routing/{Router,Tests}' + + Symfony\Component\Config\: + resource: '../../vendor/symfony/config/*' + exclude: '../../vendor/symfony/config/{FileLocator,Tests}' + + Symfony\Component\Config\Loader\LoaderInterface: + alias: Symfony\Component\Routing\Loader\YamlFileLoader + + Symfony\Component\Config\FileLocatorInterface: + alias: Symfony\Component\Config\FileLocator + + Symfony\Component\Config\FileLocator: + arguments: + - '%kernel.directory.config%' + + Symfony\Component\Routing\Router: + arguments: + $loader: '@Symfony\Component\Routing\Loader\YamlFileLoader' + $resource: 'routes.yaml' + $options: + cache_dir: '%kernel.directory.cache%/%kernel.environment%' + debug: '%kernel.debug%' +``` + +### Routes configs +```yaml +services: + _defaults: + autowire: true + + router: + alias: Symfony\Component\Routing\Router + + Symfony\Component\Routing\: + resource: '../../vendor/symfony/routing/*' + exclude: '../../vendor/symfony/routing/{Router,Tests}' + + Symfony\Component\Config\: + resource: '../../vendor/symfony/config/*' + exclude: '../../vendor/symfony/config/{FileLocator,Tests}' + + Symfony\Component\Config\Loader\LoaderInterface: + alias: Symfony\Component\Routing\Loader\YamlFileLoader + + Symfony\Component\Config\FileLocatorInterface: + alias: Symfony\Component\Config\FileLocator + + Symfony\Component\Config\FileLocator: + arguments: + - '%kernel.directory.config%' + + Symfony\Component\Routing\Router: + arguments: + $loader: '@Symfony\Component\Routing\Loader\YamlFileLoader' + $resource: 'routes.yaml' + $options: + cache_dir: '%kernel.directory.cache%/%kernel.environment%' + debug: '%kernel.debug%' + +``` + +### Components configs +```yaml +services: + _defaults: + autowire: true + + Nyholm\Psr7\: + resource: '%kernel.directory.root%/vendor/nyholm/psr7/src/*' + + #~ PSR + Eureka\Component\Http\: + resource: '%kernel.directory.root%/vendor/eureka/component-http/src/*' + + Psr\Log\: + resource: '%kernel.directory.root%/vendor/psr/log/src/*' + + Psr\Cache\CacheItemPoolInterface: + alias: Symfony\Component\Cache\Adapter\ArrayAdapter + + Symfony\Component\Cache\Adapter\ArrayAdapter: ~ + +``` + + +### Routes configs +```yaml +# ===== HOME ===== +home: + path: / + controller: Application\Controller\ExampleController::home + methods: [ GET ] + defaults: + rateLimiterQuota: 100 # Used to set rate limit on this endpoint + rateLimiterTTL: 60 # Used to set rate limit on this endpoint + +test_url_with_param: + path: /blog/{id}/{title} + controller: Application\Controller\ExampleController::viewWithParams + methods: [ GET ] + defaults: + someString: 'value' + someBool: true + someInt: 42 + requirements: + id: '\d+' + title: '[a-zA-Z-]+' + +``` + +Default value & requirements values are injected in controller actions, with the following order: +- ServerRequest object (always pass) +- defaults (in same order as defined in yaml file) +- requirements (in same order as defined in yaml. Requirement are always injected as string) + + +**Example of controller:** +```php +getResponse('Hello world!'); + } + + public function viewWithParams( + ServerRequestInterface $serverRequest, + string $someString, + bool $someBool, + int $someInt, + string $id, + string $title, + ): ResponseInterface { + return $this->getResponse("entity id: $id, title: $title, someString: $someString, someBool: " . ($someBool ? 'true' : 'false') . ", someInt: $someInt"); + } + +``` + + ## Contributing See the [CONTRIBUTING](CONTRIBUTING.md) file. @@ -81,34 +405,40 @@ NB: For the components, the `composer.lock` file is not committed. #### Tests You can run unit tests (with coverage) on your side with following command: ```bash -make tests +make php/tests ``` -You can run functional tests (without coverage) on your side with following command: +You can run integration tests (without coverage) on your side with following command: ```bash -make integration +make php/integration ``` For prettier output (but without coverage), you can use the following command: ```bash -make testdox # run tests without coverage reports but with prettified output +make php/testdox # run tests without coverage reports but with prettified output ``` #### Code Style You also can run code style check with following commands: ```bash -make phpcs +make php/check ``` You also can run code style fixes with following commands: ```bash -make phpcsf +make php/fix +``` + +#### Check for missing explicit dependencies +You can check if any explicit dependency is missing with the following command: +```bash +make php/deps ``` #### Static Analysis To perform a static analyze of your code (with phpstan, lvl 9 at default), you can use the following command: ```bash -make analyze +make php/analyse ``` To ensure you code still compatible with current supported version at Deezer and futures versions of php, you need to @@ -116,12 +446,12 @@ run the following commands (both are required for full support): Minimal supported version: ```bash -make php74compatibility +make php/min-compatibility ``` Maximal supported version: ```bash -make php83compatibility +make php/max-compatibility ``` #### CI Simulation @@ -132,4 +462,4 @@ make ci ## License -This project is currently under The MIT License (MIT). See [LICENCE](LICENSE) file for more information. +This project is currently under The MIT License (MIT). See [LICENSE](LICENSE) file for more information. diff --git a/ci/composer-dependency-analyser.php b/ci/composer-dependency-analyser.php new file mode 100644 index 0000000..c12ce95 --- /dev/null +++ b/ci/composer-dependency-analyser.php @@ -0,0 +1,15 @@ +addPathToScan(__DIR__ . '/../src', isDev: false) + ->addPathToScan(__DIR__ . '/../tests', isDev: true) + + ->ignoreErrorsOnPackages(['symfony/finder', 'symfony/yaml'], [ErrorType::UNUSED_DEPENDENCY]) // required in dependencies of symfony/dependency-injection + + ->ignoreUnknownFunctions(['apache_request_headers']) +; diff --git a/ci/composer-require-checker.json b/ci/composer-require-checker.json deleted file mode 100644 index 0383d6f..0000000 --- a/ci/composer-require-checker.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "symbol-whitelist": [ - "apache_request_headers" - ] -} diff --git a/ci/php81-compatibility.neon b/ci/phpmax-compatibility.neon similarity index 92% rename from ci/php81-compatibility.neon rename to ci/phpmax-compatibility.neon index fbd2148..085b5a0 100644 --- a/ci/php81-compatibility.neon +++ b/ci/phpmax-compatibility.neon @@ -3,7 +3,7 @@ includes: - ./../vendor/phpstan/phpstan-phpunit/rules.neon parameters: - phpVersion: 80100 + phpVersion: 80500 level: 0 paths: - ./../src diff --git a/ci/php83-compatibility.neon b/ci/phpmin-compatibility.neon similarity index 100% rename from ci/php83-compatibility.neon rename to ci/phpmin-compatibility.neon diff --git a/composer.json b/composer.json index 4dd1780..493ca04 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ }, "autoload-dev": { "psr-4": { - "Eureka\\Kernel\\Http\\Tests\\Unit\\": "./tests/unit" + "Eureka\\Kernel\\Http\\Tests\\": "./tests" } }, @@ -28,17 +28,15 @@ }, "require": { - "php": "8.1.*||8.2.*||8.3.*", + "php": ">=8.3", "ext-json": "*", "ext-filter": "*", - "nyholm/psr7": "^1.8", - - "symfony/config": "^5.4||^6.4||^7.0", - "symfony/dependency-injection": "^5.4||^6.4||^7.0", - "symfony/finder": "^5.4||^6.4||^7.0", - "symfony/routing": "^5.4||^6.4||^7.0", - "symfony/yaml": "^5.4||^6.4||^7.0", + "symfony/config": "^6.0||^7.0||^8.0", + "symfony/dependency-injection": "^6.0||^7.0||^8.0", + "symfony/finder": "^6.0||^7.0||^8.0", + "symfony/routing": "^6.0||^7.0||^8.0", + "symfony/yaml": "^6.0||^7.0||^8.0", "psr/log": "^1.1||^2.0||^3.0", "psr/cache": "^1.0||^2.0||^3.0", @@ -48,17 +46,19 @@ "psr/http-server-handler": "^1.0", "psr/http-server-middleware": "^1.0", - "eureka/component-http": "^5.3" + "eureka/component-http": "^5.0||^6.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.49.0", - "maglnet/composer-require-checker": "^4.7.1", - "phpstan/phpstan": "^1.10.59", - "phpstan/phpstan-phpunit": "^1.3.15", - "phpstan/phpstan-strict-rules": "^1.5.2", - "phpunit/phpcov": "^9.0.2", - "phpunit/phpunit": "^10.5.10", - "symfony/cache": "^5.4.35||^6.4.3||^7.0" + "friendsofphp/php-cs-fixer": "^3.92.4", + "nyholm/psr7": "^1.8.2", + "phpstan/phpstan": "^2.1.33", + "phpstan/phpstan-deprecation-rules": "^2.0.3", + "phpstan/phpstan-phpunit": "^2.0.11", + "phpstan/phpstan-strict-rules": "^2.0.7", + "phpunit/phpcov": "^11.0.3", + "phpunit/phpunit": "^12.5.4", + "shipmonk/composer-dependency-analyser": "^1.8.4", + "symfony/cache": "^6.4||^7.4.3||^8.0" } } diff --git a/config/kernel.yaml b/config/kernel.yaml index a6067af..c61ec77 100644 --- a/config/kernel.yaml +++ b/config/kernel.yaml @@ -4,16 +4,16 @@ parameters: kernel.debug: false #~ Environment - kernel.environment: prod + kernel.environment: 'prod' # Overridden in Kernel class, but define here as helper #~ Errors - kernel.error.display: 'false' + kernel.error.display: 'false' kernel.error.reporting: !php/const E_ALL #~ Directories - kernel.directory.root: '..' # Overridden in Kernel class, but define here as helper - kernel.directory.cache: '%kernel.directory.root%/var/cache' - kernel.directory.log: '%kernel.directory.root%/var/log' + kernel.directory.root: '..' # Overridden in Kernel class, but define here as helper + kernel.directory.cache: '%kernel.directory.root%/var/cache' + kernel.directory.log: '%kernel.directory.root%/var/log' kernel.directory.config: '%kernel.directory.root%/config' kernel.directory.vendor: '%kernel.directory.root%/vendor' diff --git a/config/packages/application.yaml b/config/packages/application.yaml new file mode 100644 index 0000000..c088e1f --- /dev/null +++ b/config/packages/application.yaml @@ -0,0 +1,31 @@ +services: + _defaults: + autowire: true + public: true # Important, because Controller middleware use container to get appropriate controller and use it + + #~ Application controllers + Eureka\Kernel\Http\Controller\: + resource: '../../src/Controller/*' + calls: + - setLogger: [ '@logger' ] + - setRouter: [ '@router' ] + - setResponseFactory: [ '@response_factory' ] + - setRequestFactory: [ '@request_factory' ] + - setServerRequestFactory: [ '@server_request_factory' ] + - setStreamFactory: [ '@stream_factory' ] + - setUriFactory: [ '@uri_factory' ] + + #~ Test Application controllers + Eureka\Kernel\Http\Tests\Unit\Mock\TestController: + calls: + - setLogger: [ '@logger' ] + - setRouter: [ '@router' ] + - setResponseFactory: [ '@response_factory' ] + - setRequestFactory: [ '@request_factory' ] + - setServerRequestFactory: [ '@server_request_factory' ] + - setStreamFactory: [ '@stream_factory' ] + - setUriFactory: [ '@uri_factory' ] + + #~ Auto discovery services + Eureka\Kernel\Http\Service\: + resource: '../../src/Service/*' diff --git a/config/packages/components.yaml b/config/packages/components.yaml index f8dcd7d..87bc608 100644 --- a/config/packages/components.yaml +++ b/config/packages/components.yaml @@ -1,42 +1,17 @@ -# default configuration for services in *this* file services: _defaults: autowire: true - autoconfigure: true - public: false Nyholm\Psr7\: resource: '%kernel.directory.root%/vendor/nyholm/psr7/src/*' - #~ PSR-17 Factories - response_factory: - alias: Nyholm\Psr7\Factory\Psr17Factory - public: true - - request_factory: - alias: Nyholm\Psr7\Factory\Psr17Factory - public: true - - server_request_factory: - alias: Nyholm\Psr7\Factory\Psr17Factory - public: true - - stream_factory: - alias: Nyholm\Psr7\Factory\Psr17Factory - public: true - - uri_factory: - alias: Nyholm\Psr7\Factory\Psr17Factory - public: true - #~ PSR - Psr\Log\: - resource: '%kernel.directory.root%/vendor/psr/log/src/*' - public: false - Eureka\Component\Http\: resource: '%kernel.directory.root%/vendor/eureka/component-http/src/*' + Psr\Log\: + resource: '%kernel.directory.root%/vendor/psr/log/src/*' + Psr\Cache\CacheItemPoolInterface: alias: Symfony\Component\Cache\Adapter\ArrayAdapter diff --git a/config/packages/middlewares.yaml b/config/packages/middlewares.yaml new file mode 100644 index 0000000..2eb7931 --- /dev/null +++ b/config/packages/middlewares.yaml @@ -0,0 +1,23 @@ +parameters: + + # app.middleware Name is important, used by app to get list of middlewares + # order is important, as the first is process, then the following, etc. + app.middleware: + error: 'Eureka\Kernel\Http\Middleware\ErrorMiddleware' + router: 'Eureka\Kernel\Http\Middleware\RouterMiddleware' + logger: 'Eureka\Kernel\Http\Middleware\ResponseTimeLoggerMiddleware' + quota: 'Eureka\Kernel\Http\Middleware\RateLimiterMiddleware' + controller: 'Eureka\Kernel\Http\Middleware\ControllerMiddleware' + +services: + _defaults: + autowire: true + public: true # Must be true, because Application use container to retrieve each middleware and assign them to handler + bind: + $applicationName: '%app.name%' + $logger: '@logger' + $cache: '@cache' + + #~ Global Kernel Services Middleware + Eureka\Kernel\Http\Middleware\: + resource: '../../src/Middleware' diff --git a/config/packages/router.yaml b/config/packages/router.yaml index 3f1b8c1..c4c7ae3 100644 --- a/config/packages/router.yaml +++ b/config/packages/router.yaml @@ -1,21 +1,17 @@ services: - # default configuration for services in *this* file _defaults: autowire: true - autoconfigure: true - public: false router: - public: true alias: Symfony\Component\Routing\Router Symfony\Component\Routing\: resource: '../../vendor/symfony/routing/*' - exclude: '../../vendor/symfony/routing/{Router,Tests}' + exclude: '../../vendor/symfony/routing/{Router,Tests}' Symfony\Component\Config\: resource: '../../vendor/symfony/config/*' - exclude: '../../vendor/symfony/config/{FileLocator,Tests}' + exclude: '../../vendor/symfony/config/{FileLocator,Tests}' Symfony\Component\Config\Loader\LoaderInterface: alias: Symfony\Component\Routing\Loader\YamlFileLoader @@ -29,8 +25,8 @@ services: Symfony\Component\Routing\Router: arguments: - $loader: '@Symfony\Component\Routing\Loader\YamlFileLoader' + $loader: '@Symfony\Component\Routing\Loader\YamlFileLoader' $resource: 'routes.yaml' $options: - cache_dir: '%kernel.directory.cache%' - debug: true + cache_dir: '%kernel.directory.cache%/%kernel.environment%' + debug: '%kernel.debug%' diff --git a/config/routes.yaml b/config/routes.yaml index 621bf45..63b82df 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -22,6 +22,18 @@ test_html: defaults: someDefault: 'value' +test_url_with_param: + path: /test/entities/{id}/{title} + controller: Eureka\Kernel\Http\Tests\Unit\Mock\TestController::testUrlAction + methods: [ GET ] + defaults: + someString: 'value' + someBool: true + someInt: 42 + requirements: + id: '\d+' + title: '[a-zA-Z-]+' + # ===== ERRORS ===== test_error_html_internal: path: /test/error/html/internal-server-error diff --git a/config/services.yaml b/config/services.yaml index a2a49de..d2b1c0a 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -1,55 +1,36 @@ -# Services Yaml file parameters: - app.name: 'kernel' - app.middleware: - logger: 'Eureka\Kernel\Http\Middleware\ResponseTimeLoggerMiddleware' - error: 'Eureka\Kernel\Http\Middleware\ErrorMiddleware' - router: 'Eureka\Kernel\Http\Middleware\RouterMiddleware' - quota: 'Eureka\Kernel\Http\Middleware\RateLimiterMiddleware' - controller: 'Eureka\Kernel\Http\Middleware\ControllerMiddleware' - services: - # default configuration for services in *this* file - _defaults: - autowire: true - autoconfigure: true - public: true - bind: - $applicationName: '%app.name%' - $logger: '@Psr\Log\NullLogger' - $cache: '@Psr\Cache\CacheItemPoolInterface' - + # Main container Psr\Container\ContainerInterface: alias: 'service_container' - #~ Middleware - Eureka\Kernel\Http\Middleware\: - resource: '../src/Middleware/*' - - #~ Middleware - Eureka\Kernel\Http\Service\: - resource: '../src/Service/*' - - #~ Application controllers - Eureka\Kernel\Http\Tests\Unit\Mock\TestController: - calls: - - [ 'setLogger', [ '@Psr\Log\NullLogger' ] ] - - [ 'setRouter', [ '@router' ] ] - - [ 'setResponseFactory', [ '@response_factory' ] ] - - [ 'setRequestFactory', [ '@request_factory' ] ] - - [ 'setServerRequestFactory', [ '@server_request_factory' ] ] - - [ 'setStreamFactory', [ '@stream_factory' ] ] - - [ 'setUriFactory', [ '@uri_factory' ] ] - - #~ Application controllers - Eureka\Kernel\Http\Controller\: - resource: '../src/Controller/*' - calls: - - [ 'setRouter', [ '@router' ] ] - - [ 'setResponseFactory', [ '@response_factory' ] ] - - [ 'setRequestFactory', [ '@request_factory' ] ] - - [ 'setServerRequestFactory', [ '@server_request_factory' ] ] - - [ 'setStreamFactory', [ '@stream_factory' ] ] - - [ 'setUriFactory', [ '@uri_factory' ] ] + # Main Logger + logger: + alias: Psr\Log\NullLogger + + # Main cache item pool + cache: + alias: Psr\Cache\CacheItemPoolInterface + + #~ PSR-17 Factories public alias (used by Application via container directly) + response_factory: + alias: Nyholm\Psr7\Factory\Psr17Factory + public: true + + request_factory: + alias: Nyholm\Psr7\Factory\Psr17Factory + public: true + + server_request_factory: + alias: Nyholm\Psr7\Factory\Psr17Factory + public: true + + stream_factory: + alias: Nyholm\Psr7\Factory\Psr17Factory + public: true + + uri_factory: + alias: Nyholm\Psr7\Factory\Psr17Factory + public: true diff --git a/config/test/services.yaml b/config/test/services.yaml new file mode 100644 index 0000000..1ff6ebc --- /dev/null +++ b/config/test/services.yaml @@ -0,0 +1,5 @@ +parameters: + #~ Override list of middlewares for in some tests + app.middleware: + router: 'Eureka\Kernel\Http\Middleware\RouterMiddleware' + controller: 'Eureka\Kernel\Http\Middleware\ControllerMiddleware' diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 0c8bddd..2eb7d22 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,12 +1,12 @@ includes: - ./vendor/phpstan/phpstan-phpunit/extension.neon - ./vendor/phpstan/phpstan-phpunit/rules.neon + - ./vendor/phpstan/phpstan-deprecation-rules/rules.neon - ./vendor/phpstan/phpstan-strict-rules/rules.neon - - ./vendor/phpstan/phpstan/conf/bleedingEdge.neon + - phar://phpstan.phar/conf/bleedingEdge.neon parameters: - #~ Global conf - phpVersion: 80100 + phpVersion: 80300 level: max paths: - ./src @@ -16,15 +16,9 @@ parameters: - ./vendor/autoload.php #~ Rules - treatPhpDocTypesAsCertain: false - checkAlwaysTrueInstanceof: false + #treatPhpDocTypesAsCertain: false strictRules: - noVariableVariables: false + noVariableVariables: false # Necessary to call the controller action #~ Errors - ignoreErrors: - - '`Construct empty\(\) is not allowed. Use more strict comparison.`' # Globally used in conscientious way - - message: '`Call to (static )?method PHPUnit\\Framework\\Assert::assert.+ will always evaluate to true`' - path: ./tests/unit/ - - message: '`Strict comparison using === between array and false will always evaluate to false`' - path: ./src/Application/Application.php + ignoreErrors: [] diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0662af6..aeb9020 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,29 +1,26 @@ - - - ./tests/unit - - + displayDetailsOnTestsThatTriggerDeprecations="true" + cacheDirectory="build/.phpunit.cache" +> + ./src + + + + ./tests/Unit + + + ./tests/Integration + + + diff --git a/phpunit.xml.dist.bak b/phpunit.xml.dist.bak deleted file mode 100644 index dac4f67..0000000 --- a/phpunit.xml.dist.bak +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - ./src - - - - - - ./tests/unit - - - - diff --git a/src/Application/Application.php b/src/Application/Application.php index e9f45bb..0509d99 100644 --- a/src/Application/Application.php +++ b/src/Application/Application.php @@ -25,9 +25,6 @@ use Psr\Http\Server\MiddlewareInterface; /** - * Application class - * - * @author Romain Cottard * @phpstan-type ServerParams array{ * REQUEST_METHOD?: string, * HTTPS?: string, @@ -42,23 +39,10 @@ class Application implements ApplicationInterface { /** @var MiddlewareInterface[] $middleware */ protected array $middleware = []; - protected Kernel $kernel; - /** - * Application constructor. - * - * @param Kernel $kernel - */ - public function __construct(Kernel $kernel) - { - $this->kernel = $kernel; - } + public function __construct(protected readonly Kernel $kernel) {} - /** - * @param ServerRequestInterface|null $serverRequest - * @return ResponseInterface - */ - public function run(ServerRequestInterface $serverRequest = null): ResponseInterface + public function run(?ServerRequestInterface $serverRequest = null): ResponseInterface { try { $serverRequest = $serverRequest ?? $this->createServerRequest(); @@ -69,7 +53,7 @@ public function run(ServerRequestInterface $serverRequest = null): ResponseInter //~ Get response through middlewares $handler = new Http\Server\RequestHandler($response, $this->middleware); $response = $handler->handle($serverRequest); - } catch (\Exception $exception) { // @codeCoverageIgnore + } catch (\Throwable $exception) { // @codeCoverageIgnore // @codeCoverageIgnoreStart //~ Catch not handled exception - Should not happen $serverRequest = $serverRequest ?? $this->createServerRequest(false); @@ -87,7 +71,7 @@ public function run(ServerRequestInterface $serverRequest = null): ResponseInter /** @var bool $debug */ $debug = $this->kernel->getContainer()->getParameter('kernel.debug'); - $controller->setEnvironment((string) $env, (bool) $debug); + $controller->setEnvironment($env, $debug); $response = $controller->error($serverRequest, $exception); // @codeCoverageIgnoreEnd @@ -96,10 +80,6 @@ public function run(ServerRequestInterface $serverRequest = null): ResponseInter return $response; } - /** - * @param ResponseInterface $response - * @return $this - */ public function send(ResponseInterface $response): ApplicationInterface { //~ Write Headers @@ -110,7 +90,7 @@ public function send(ResponseInterface $response): ApplicationInterface 'HTTP/%s %s %s', $response->getProtocolVersion(), $response->getStatusCode(), - $response->getReasonPhrase() + $response->getReasonPhrase(), ); \header($header, true, $response->getStatusCode()); @@ -129,11 +109,6 @@ public function send(ResponseInterface $response): ApplicationInterface return $this; } - /** - * Load middleware - * - * @return void - */ private function loadMiddleware(): void { $this->middleware = []; @@ -149,10 +124,6 @@ private function loadMiddleware(): void } } - /** - * @param ServerRequestInterface $serverRequest - * @return ResponseInterface - */ private function createResponse(ServerRequestInterface $serverRequest): ResponseInterface { /** @var ResponseFactoryInterface $responseFactory */ @@ -162,8 +133,8 @@ private function createResponse(ServerRequestInterface $serverRequest): Response //~ Automatic add "application/json" to response header when client accept "json" in response. if ( - $serverRequest->hasHeader('Accept') && - \in_array('application/json', $serverRequest->getHeader('Accept'), true) // @codeCoverageIgnore + $serverRequest->hasHeader('Accept') + && \in_array('application/json', $serverRequest->getHeader('Accept'), true) // @codeCoverageIgnore ) { $response = $response->withAddedHeader('Content-Type', 'application/json'); // @codeCoverageIgnore } @@ -171,17 +142,13 @@ private function createResponse(ServerRequestInterface $serverRequest): Response return $response; } - /** - * @param bool $withBody - * @return ServerRequestInterface - */ private function createServerRequest(bool $withBody = true): ServerRequestInterface { $serverRequestFactory = $this->getServerRequestFactory(); /** @var ServerParams $params */ $params = $_SERVER; - $method = !empty($params['REQUEST_METHOD']) ? $params['REQUEST_METHOD'] : 'GET'; + $method = ($params['REQUEST_METHOD'] ?? '') !== '' ? $params['REQUEST_METHOD'] : 'GET'; //~ Create server request $serverRequest = $serverRequestFactory->createServerRequest($method, $this->createUri(), $_SERVER); @@ -216,9 +183,6 @@ private function createServerRequest(bool $withBody = true): ServerRequestInterf return $serverRequest; } - /** - * @return UriInterface - */ private function createUri(): UriInterface { $uriFactory = $this->getUriFactory(); @@ -257,19 +221,15 @@ private function createUri(): UriInterface } /** - * @return array + * @return array * * @codeCoverageIgnore */ private function getHeaders(): array { $headers = []; - if (function_exists('apache_request_headers')) { + if (\function_exists('apache_request_headers')) { $headers = \apache_request_headers(); - - if ($headers === false) { - $headers = []; - } } foreach ($headers as $name => $header) { @@ -280,6 +240,7 @@ private function getHeaders(): array $headers[$name] = $header; } + /** @var array $headers */ return $headers; } @@ -299,15 +260,16 @@ private function getParsedBody(array $contentTypes): array } $requestBody = \file_get_contents('php://input'); + $isEmptyBody = $requestBody === false || $requestBody === ''; try { - $parsedBody = !empty($requestBody) ? json_decode($requestBody, true, 512, JSON_THROW_ON_ERROR) : []; + $parsedBody = !$isEmptyBody ? \json_decode($requestBody, true, 512, \JSON_THROW_ON_ERROR) : []; // @codeCoverageIgnoreStart } catch (\JsonException) { $parsedBody = []; } // @codeCoverageIgnoreEnd - if (!empty($requestBody) && empty($parsedBody)) { + if (!$isEmptyBody && $parsedBody === []) { \parse_str($requestBody, $parsedBody); // @codeCoverageIgnore } @@ -317,7 +279,6 @@ private function getParsedBody(array $contentTypes): array /** * @param array $contentTypes - * @return bool */ private function isRequestBodyForm(array $contentTypes): bool { @@ -333,7 +294,6 @@ private function isRequestBodyForm(array $contentTypes): bool /** * @param array $contentTypes - * @return bool */ private function isRequestBodyJson(array $contentTypes): bool { @@ -347,44 +307,33 @@ private function isRequestBodyJson(array $contentTypes): bool return false; } - /** - * @return ResponseFactoryInterface - * @codeCoverageIgnore - */ private function getResponseFactory(): ResponseFactoryInterface { $factory = $this->kernel->getContainer()->get('response_factory'); if (!($factory instanceof ResponseFactoryInterface)) { - throw new \LogicException('Service "response_factory" not a ' . ResponseFactoryInterface::class); + throw new \LogicException('Service "response_factory" not a ' . ResponseFactoryInterface::class); // @codeCoverageIgnore } return $factory; } - /** - * @return RequestFactoryInterface - * @codeCoverageIgnore - */ private function getRequestFactory(): RequestFactoryInterface { $factory = $this->kernel->getContainer()->get('request_factory'); if (!($factory instanceof RequestFactoryInterface)) { - throw new \LogicException('Service "request_factory" not a ' . RequestFactoryInterface::class); + throw new \LogicException('Service "request_factory" not a ' . RequestFactoryInterface::class); // @codeCoverageIgnore } return $factory; } - /** - * @return ServerRequestFactoryInterface - */ private function getServerRequestFactory(): ServerRequestFactoryInterface { $factory = $this->kernel->getContainer()->get('server_request_factory'); if (!($factory instanceof ServerRequestFactoryInterface)) { // @codeCoverageIgnoreStart throw new \LogicException( - 'Service "server_request_factory" not a ' . ServerRequestFactoryInterface::class + 'Service "server_request_factory" not a ' . ServerRequestFactoryInterface::class, ); // @codeCoverageIgnoreEnd } @@ -392,30 +341,23 @@ private function getServerRequestFactory(): ServerRequestFactoryInterface return $factory; } - /** - * @return StreamFactoryInterface - * @codeCoverageIgnore - */ private function getStreamFactory(): StreamFactoryInterface { $factory = $this->kernel->getContainer()->get('stream_factory'); if (!($factory instanceof StreamFactoryInterface)) { - throw new \LogicException('Service "stream_factory" not a ' . StreamFactoryInterface::class); + throw new \LogicException('Service "stream_factory" not a ' . StreamFactoryInterface::class); // @codeCoverageIgnore } return $factory; } - /** - * @return UriFactoryInterface - */ private function getUriFactory(): UriFactoryInterface { $factory = $this->kernel->getContainer()->get('uri_factory'); if (!($factory instanceof UriFactoryInterface)) { // @codeCoverageIgnoreStart throw new \LogicException( - 'Service "uri_factory" not a ' . UriFactoryInterface::class + 'Service "uri_factory" not a ' . UriFactoryInterface::class, ); // @codeCoverageIgnoreEnd } diff --git a/src/Application/ApplicationInterface.php b/src/Application/ApplicationInterface.php index d1e9572..45bae61 100644 --- a/src/Application/ApplicationInterface.php +++ b/src/Application/ApplicationInterface.php @@ -25,7 +25,7 @@ interface ApplicationInterface * @param ServerRequestInterface|null $serverRequest * @return ResponseInterface */ - public function run(ServerRequestInterface $serverRequest = null): ResponseInterface; + public function run(?ServerRequestInterface $serverRequest = null): ResponseInterface; /** * Send response to client diff --git a/src/Controller/Controller.php b/src/Controller/Controller.php index aefadef..4ed4b04 100644 --- a/src/Controller/Controller.php +++ b/src/Controller/Controller.php @@ -18,11 +18,6 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -/** - * Controller class - * - * @author Romain Cottard - */ abstract class Controller implements ControllerInterface { use HttpFactoryAwareTrait; @@ -34,7 +29,7 @@ abstract class Controller implements ControllerInterface * @var string[] List of official Http Code (excluding WebDAV official codes) * @see https://en.wikipedia.org/wiki/List_of_HTTP_status_codes */ - protected const HTTP_CODE_MESSAGES = [ + protected const array HTTP_CODE_MESSAGES = [ //~ 2xx Success 200 => 'Success', 201 => 'Created', @@ -99,11 +94,6 @@ abstract class Controller implements ControllerInterface private bool $debug = false; private string $environment = 'prod'; - /** - * @param string $environment - * @param bool $isDebug - * @return ControllerInterface - */ public function setEnvironment(string $environment, bool $isDebug = false): ControllerInterface { $this->environment = $environment; @@ -112,65 +102,36 @@ public function setEnvironment(string $environment, bool $isDebug = false): Cont return $this; } - /** - * This method is executed before the main controller action method. - * - * @param ServerRequestInterface|null $serverRequest - * @return void - */ public function preAction(?ServerRequestInterface $serverRequest = null): void { //~ Automatically add server request to controller in pre-action - if (!empty($serverRequest)) { + if ($serverRequest !== null) { $this->setServerRequest($serverRequest); } } - /** - * This method is executed after the main controller action method. - * - * @param ServerRequestInterface|null $serverRequest - * @return void - */ public function postAction(?ServerRequestInterface $serverRequest = null): void {} - /** - * @return bool - */ protected function isDebug(): bool { return $this->debug; } - /** - * @return bool - */ protected function isDev(): bool { - return ($this->environment === 'dev'); + return $this->environment === 'dev'; } - /** - * @return bool - */ protected function isProd(): bool { - return ($this->environment === 'prod'); + return $this->environment === 'prod'; } - /** - * @return string - */ protected function getEnvironment(): string { return $this->environment; } - /** - * @param string $content - * @param int $code - * @return ResponseInterface - */ protected function getResponse(string $content, int $code = 200): ResponseInterface { $response = $this->getResponseFactory()->createResponse($code); @@ -179,39 +140,29 @@ protected function getResponse(string $content, int $code = 200): ResponseInterf return $response; } - /** - * @param \stdClass|string|int|float|array $content - * @param int $code - * @param bool $jsonEncode - * @return ResponseInterface - */ - protected function getResponseJson($content, int $code = 200, bool $jsonEncode = true): ResponseInterface + protected function getResponseJson(mixed $content, int $code = 200, bool $jsonEncode = true): ResponseInterface { - if ($jsonEncode || (!is_string($content) && !is_numeric($content))) { - $content = json_encode($content); + if ($jsonEncode || (!\is_string($content) && !\is_numeric($content))) { + $content = \json_encode($content); } return $this->getResponse((string) $content, $code)->withAddedHeader('Content-Type', 'application/json'); } /** - * Redirect on specified url. - * - * @param string $url - * @param int $status - * @return void * @codeCoverageIgnore */ - protected function redirect(string $url, int $status = 301): void + protected function redirect(string $url, int $status = 301): never { - if (!empty($url)) { + if ($url !== '') { $params = $this->getServerRequest()->getServerParams(); - $protocolVersion = str_replace('HTTP/', '', (string) ($params['SERVER_PROTOCOL'] ?? '1.1')); + $serverProtocol = !isset($params['SERVER_PROTOCOL']) || !\is_string($params['SERVER_PROTOCOL']) ? '1.1' : $params['SERVER_PROTOCOL']; + $protocolVersion = \str_replace('HTTP/', '', $serverProtocol); - header('HTTP/' . $protocolVersion . ' ' . $status . ' Redirect'); - header('Status: ' . $status . ' Redirect'); - header('Location: ' . $url); - header('Pragma: no-cache'); + \header('HTTP/' . $protocolVersion . ' ' . $status . ' Redirect'); + \header('Status: ' . $status . ' Redirect'); + \header('Location: ' . $url); + \header('Pragma: no-cache'); exit(0); } else { throw new \InvalidArgumentException('Url is empty !'); @@ -219,13 +170,10 @@ protected function redirect(string $url, int $status = 301): void } /** - * @param string $routeName * @param array $params - * @param int $status - * @return void * @codeCoverageIgnore */ - protected function redirectToRoute(string $routeName, array $params = [], int $status = 200): void + protected function redirectToRoute(string $routeName, array $params = [], int $status = 200): never { $this->redirect($this->getRouteUri($routeName, $params), $status); } diff --git a/src/Controller/ControllerInterface.php b/src/Controller/ControllerInterface.php index 05557a0..038e99b 100644 --- a/src/Controller/ControllerInterface.php +++ b/src/Controller/ControllerInterface.php @@ -13,34 +13,22 @@ use Psr\Http\Message\ServerRequestInterface; -/** - * Controller interface - * - * @author Romain Cottard - */ interface ControllerInterface { /** * Set current route data. * * @param array $route - * @return void */ public function setRoute(array $route): void; /** * This method is executed before the main controller action method. - * - * @param null|ServerRequestInterface $serverRequest - * @return void */ public function preAction(?ServerRequestInterface $serverRequest = null): void; /** * This method is executed after the main controller action method. - * - * @param null|ServerRequestInterface $serverRequest - * @return void */ public function postAction(?ServerRequestInterface $serverRequest = null): void; } diff --git a/src/Controller/ErrorController.php b/src/Controller/ErrorController.php index 1775ad1..5c78c01 100644 --- a/src/Controller/ErrorController.php +++ b/src/Controller/ErrorController.php @@ -22,48 +22,21 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -/** - * Controller class - * - * @author Romain Cottard - */ class ErrorController extends Controller implements ErrorControllerInterface { - /** - * @param ServerRequestInterface $serverRequest - * @param \Exception $exception - * @return ResponseInterface - */ - public function error(ServerRequestInterface $serverRequest, \Exception $exception): ResponseInterface + public function error(ServerRequestInterface $serverRequest, \Throwable $exception): ResponseInterface { - switch (true) { - case $exception instanceof HttpBadRequestException: - $httpCode = 400; - break; - case $exception instanceof HttpUnauthorizedException: - $httpCode = 401; - break; - case $exception instanceof HttpForbiddenException: - $httpCode = 403; - break; - case $exception instanceof HttpNotFoundException: - $httpCode = 404; - break; - case $exception instanceof HttpMethodNotAllowedException: - $httpCode = 405; - break; - case $exception instanceof HttpConflictException: - $httpCode = 409; - break; - case $exception instanceof HttpTooManyRequestsException: - $httpCode = 429; - break; - case $exception instanceof HttpServiceUnavailableException: - $httpCode = 503; - break; - default: - $httpCode = 500; - } + $httpCode = match (true) { + $exception instanceof HttpBadRequestException => 400, + $exception instanceof HttpUnauthorizedException => 401, + $exception instanceof HttpForbiddenException => 403, + $exception instanceof HttpNotFoundException => 404, + $exception instanceof HttpMethodNotAllowedException => 405, + $exception instanceof HttpConflictException => 409, + $exception instanceof HttpTooManyRequestsException => 429, + $exception instanceof HttpServiceUnavailableException => 503, + default => 500, + }; if ($this->acceptJsonResponse()) { $content = $this->getErrorContentJson($httpCode, $exception); // @codeCoverageIgnore @@ -74,24 +47,16 @@ public function error(ServerRequestInterface $serverRequest, \Exception $excepti return $this->getResponse($content, $httpCode); } - /** - * @param ServerRequestInterface $request - * @param \Exception $exception - * @return string - */ protected function getErrorContentHtml(ServerRequestInterface $request, \Throwable $exception): string { return - '
exception[' . get_class($exception) . ']: ' . PHP_EOL .
-            $exception->getMessage() . PHP_EOL .
-            ($this->isDebug() ? $exception->getTraceAsString() . PHP_EOL : '') . PHP_EOL .
-            '
'; + '
exception[' . $exception::class . ']: ' . PHP_EOL
+            . $exception->getMessage() . PHP_EOL
+            . ($this->isDebug() ? $exception->getTraceAsString() . PHP_EOL : '') . PHP_EOL
+            . '
'; } /** - * @param int $code - * @param \Exception $exception - * @return string * @codeCoverageIgnore */ protected function getErrorContentJson(int $code, \Throwable $exception): string @@ -100,8 +65,8 @@ protected function getErrorContentJson(int $code, \Throwable $exception): string $error = [ 'status' => (string) $code, 'title' => self::HTTP_CODE_MESSAGES[$code] ?? 'Unknown', - 'code' => !empty($exception->getCode()) ? (string) $exception->getCode() : '99', - 'detail' => !empty($exception->getMessage()) ? $exception->getMessage() : 'Undefined message', + 'code' => $exception->getCode() !== 0 ? (string) $exception->getCode() : '99', + 'detail' => $exception->getMessage() !== '' ? $exception->getMessage() : 'Undefined message', ]; if ($this->isDebug()) { diff --git a/src/Controller/ErrorControllerInterface.php b/src/Controller/ErrorControllerInterface.php index 968ed16..7c43208 100644 --- a/src/Controller/ErrorControllerInterface.php +++ b/src/Controller/ErrorControllerInterface.php @@ -14,17 +14,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -/** - * ErrorControllerInterface interface - * - * @author Romain Cottard - */ interface ErrorControllerInterface extends ControllerInterface { - /** - * @param ServerRequestInterface $serverRequest - * @param \Exception $exception - * @return ResponseInterface - */ - public function error(ServerRequestInterface $serverRequest, \Exception $exception): ResponseInterface; + public function error(ServerRequestInterface $serverRequest, \Throwable $exception): ResponseInterface; } diff --git a/src/Exception/HttpBadRequestException.php b/src/Exception/HttpBadRequestException.php index efc56d0..3744f6c 100644 --- a/src/Exception/HttpBadRequestException.php +++ b/src/Exception/HttpBadRequestException.php @@ -11,9 +11,4 @@ namespace Eureka\Kernel\Http\Exception; -/** - * Class BadRequestException - * - * @author Romain Cottard - */ class HttpBadRequestException extends \RuntimeException {} diff --git a/src/Exception/HttpConflictException.php b/src/Exception/HttpConflictException.php index 57ee36e..00c71da 100644 --- a/src/Exception/HttpConflictException.php +++ b/src/Exception/HttpConflictException.php @@ -11,9 +11,4 @@ namespace Eureka\Kernel\Http\Exception; -/** - * Class HttpConflictException - * - * @author Romain Cottard - */ class HttpConflictException extends \RuntimeException {} diff --git a/src/Exception/HttpForbiddenException.php b/src/Exception/HttpForbiddenException.php index 493a6dd..a1e1f48 100644 --- a/src/Exception/HttpForbiddenException.php +++ b/src/Exception/HttpForbiddenException.php @@ -11,9 +11,4 @@ namespace Eureka\Kernel\Http\Exception; -/** - * Class HttpForbiddenException - * - * @author Romain Cottard - */ class HttpForbiddenException extends \RuntimeException {} diff --git a/src/Exception/HttpInternalServerErrorException.php b/src/Exception/HttpInternalServerErrorException.php index 5048f1b..48751e8 100644 --- a/src/Exception/HttpInternalServerErrorException.php +++ b/src/Exception/HttpInternalServerErrorException.php @@ -11,9 +11,4 @@ namespace Eureka\Kernel\Http\Exception; -/** - * Class HttpInternalServerErrorException - * - * @author Romain Cottard - */ class HttpInternalServerErrorException extends \RuntimeException {} diff --git a/src/Exception/HttpMethodNotAllowedException.php b/src/Exception/HttpMethodNotAllowedException.php index 950da0e..ae25010 100644 --- a/src/Exception/HttpMethodNotAllowedException.php +++ b/src/Exception/HttpMethodNotAllowedException.php @@ -11,9 +11,4 @@ namespace Eureka\Kernel\Http\Exception; -/** - * Class HttpMethodNotAllowedException - * - * @author Romain Cottard - */ class HttpMethodNotAllowedException extends \RuntimeException {} diff --git a/src/Exception/HttpNotFoundException.php b/src/Exception/HttpNotFoundException.php index 023082d..3fec574 100644 --- a/src/Exception/HttpNotFoundException.php +++ b/src/Exception/HttpNotFoundException.php @@ -11,9 +11,4 @@ namespace Eureka\Kernel\Http\Exception; -/** - * Class HttpNotFoundException - * - * @author Romain Cottard - */ class HttpNotFoundException extends \RuntimeException {} diff --git a/src/Exception/HttpServiceUnavailableException.php b/src/Exception/HttpServiceUnavailableException.php index 9d3e3d7..eae204a 100644 --- a/src/Exception/HttpServiceUnavailableException.php +++ b/src/Exception/HttpServiceUnavailableException.php @@ -11,9 +11,4 @@ namespace Eureka\Kernel\Http\Exception; -/** - * Class HttpServiceUnavailableException - * - * @author Romain Cottard - */ class HttpServiceUnavailableException extends \RuntimeException {} diff --git a/src/Exception/HttpTooManyRequestsException.php b/src/Exception/HttpTooManyRequestsException.php index 753b217..ec3a15d 100644 --- a/src/Exception/HttpTooManyRequestsException.php +++ b/src/Exception/HttpTooManyRequestsException.php @@ -11,9 +11,4 @@ namespace Eureka\Kernel\Http\Exception; -/** - * Class HttpTooManyRequestsException - * - * @author Romain Cottard - */ class HttpTooManyRequestsException extends \RuntimeException {} diff --git a/src/Exception/HttpUnauthorizedException.php b/src/Exception/HttpUnauthorizedException.php index 622fff7..dd449d5 100644 --- a/src/Exception/HttpUnauthorizedException.php +++ b/src/Exception/HttpUnauthorizedException.php @@ -11,9 +11,4 @@ namespace Eureka\Kernel\Http\Exception; -/** - * Class HttpUnauthorizedException - * - * @author Romain Cottard - */ class HttpUnauthorizedException extends \RuntimeException {} diff --git a/src/Kernel.php b/src/Kernel.php index 103e3b9..abd7984 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -24,7 +24,6 @@ use Symfony\Component\DependencyInjection\Loader\GlobFileLoader; use Symfony\Component\DependencyInjection\Loader\IniFileLoader; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; -use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; /** @@ -34,7 +33,7 @@ */ class Kernel { - private const CONFIG_EXTENSIONS = '.{php,xml,yaml,yml}'; + private const string CONFIG_EXTENSIONS = '.{php,yaml,yml}'; private ContainerInterface $container; private ContainerBuilder $containerBuilder; @@ -106,8 +105,8 @@ protected function initContainer(): self */ protected function initErrorReporting(int $reporting, string $display): self { - error_reporting($reporting); - ini_set('display_errors', $display); + \error_reporting($reporting); + \ini_set('display_errors', $display); return $this; } @@ -121,14 +120,14 @@ protected function overrideErrorReporting(): self { //~ Override reporting value from config $reporting = $this->container->getParameter('kernel.error.reporting'); - $errorLevel = (int) (!is_scalar($reporting) ? error_reporting(0) : $reporting); + $errorLevel = (int) (!\is_scalar($reporting) ? \error_reporting(0) : $reporting); //~ Override display value from config $display = $this->container->getParameter('kernel.error.display'); - $errorDisplay = (string) (!is_scalar($display) ? ini_get('display_errors') : $display); + $errorDisplay = (string) (!\is_scalar($display) ? \ini_get('display_errors') : $display); - error_reporting($errorLevel); - ini_set('display_errors', $errorDisplay); + \error_reporting($errorLevel); + \ini_set('display_errors', $errorDisplay); return $this; } @@ -144,11 +143,13 @@ protected function loadConfig(): self //~ Load kernel config files $loader->load($this->getConfigDir() . '/{kernel}' . self::CONFIG_EXTENSIONS, 'glob'); - $loader->load($this->getConfigDir() . '/{kernel}_' . $this->environment . self::CONFIG_EXTENSIONS, 'glob'); // @deprecated $this->containerBuilder->setParameter('kernel.environment', $this->environment); $this->containerBuilder->setParameter('kernel.directory.root', $this->rootDirectory); + //~ Load services config files + $loader->load($this->getConfigDir() . '/{services}' . self::CONFIG_EXTENSIONS, 'glob'); + //~ Load packages config files $loader->load($this->getConfigDir() . '/{packages}/*' . self::CONFIG_EXTENSIONS, 'glob'); $loader->load($this->getConfigDir() . '/{packages}/**/*' . self::CONFIG_EXTENSIONS, 'glob'); @@ -157,10 +158,6 @@ protected function loadConfig(): self $loader->load($this->getConfigDir() . '/{' . $this->environment . '}/*' . self::CONFIG_EXTENSIONS, 'glob'); $loader->load($this->getConfigDir() . '/{' . $this->environment . '}/**/*' . self::CONFIG_EXTENSIONS, 'glob'); - //~ Load services config files - $loader->load($this->getConfigDir() . '/{services}' . self::CONFIG_EXTENSIONS, 'glob'); - $loader->load($this->getConfigDir() . '/{services}_' . $this->environment . self::CONFIG_EXTENSIONS, 'glob'); // @deprecated - //~ Load secrets config files $loader->load($this->getConfigDir() . '/{secrets}/*' . self::CONFIG_EXTENSIONS, 'glob'); $loader->load($this->getConfigDir() . '/{secrets}/**/*' . self::CONFIG_EXTENSIONS, 'glob'); @@ -184,7 +181,7 @@ protected function registerCompilerPasses(): self $compilerPasses = (array) $this->containerBuilder->getParameter('kernel.compiler_pass'); /** @var class-string $compilerPass */ foreach ($compilerPasses as $compilerPass) { - if (!class_exists($compilerPass)) { + if (!\class_exists($compilerPass)) { continue; } $this->containerBuilder->addCompilerPass(new $compilerPass()); // @codeCoverageIgnore @@ -223,14 +220,13 @@ protected function getContainerLoader(ContainerBuilder $container): DelegatingLo $locator = new FileLocator(); $resolver = new LoaderResolver( [ - new XmlFileLoader($container, $locator), new YamlFileLoader($container, $locator), new IniFileLoader($container, $locator), new PhpFileLoader($container, $locator), new GlobFileLoader($container, $locator), new DirectoryLoader($container, $locator), new ClosureLoader($container), - ] + ], ); return new DelegatingLoader($resolver); @@ -243,7 +239,7 @@ protected function getContainerLoader(ContainerBuilder $container): DelegatingLo */ protected function getContainerClass(): string { - return $this->name . ucfirst($this->environment) . ($this->debug ? 'Debug' : '') . 'ProjectContainer'; + return $this->name . \ucfirst($this->environment) . ($this->debug ? 'Debug' : '') . 'ProjectContainer'; } /** @@ -289,12 +285,14 @@ private function initVarSubDir(): self ]; foreach ($dirs as $name => $dir) { - if (!is_dir($dir)) { - if (false === @mkdir($dir, 0777, true) && !is_dir($dir)) { - throw new \RuntimeException(sprintf("Unable to create the %s directory (%s)\n", $name, $dir)); // @codeCoverageIgnore + if (!\is_dir($dir)) { + // @codeCoverageIgnoreStart + if (false === \mkdir($dir, 0777, true) && !\is_dir($dir)) { + throw new \RuntimeException(\sprintf("Unable to create the %s directory (%s)\n", $name, $dir)); } - } elseif (!is_writable($dir)) { - throw new \RuntimeException(sprintf("Unable to write in the %s directory (%s)\n", $name, $dir)); // @codeCoverageIgnore + // @codeCoverageIgnoreEnd + } elseif (!\is_writable($dir)) { + throw new \RuntimeException(\sprintf("Unable to write in the %s directory (%s)\n", $name, $dir)); // @codeCoverageIgnore } } diff --git a/src/Middleware/ControllerMiddleware.php b/src/Middleware/ControllerMiddleware.php index fdbfc16..4186e8e 100644 --- a/src/Middleware/ControllerMiddleware.php +++ b/src/Middleware/ControllerMiddleware.php @@ -21,44 +21,26 @@ use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -/** - * Class ControllerMiddleware - * - * @author Romain Cottard - */ -class ControllerMiddleware implements MiddlewareInterface +readonly class ControllerMiddleware implements MiddlewareInterface { - protected ContainerInterface $container; - - /** - * ControllerMiddleware constructor. - * - * @param ContainerInterface $container - */ - public function __construct(ContainerInterface $container) - { - $this->container = $container; - } + public function __construct(private ContainerInterface $container) {} /** * Process an incoming server request and return a response, optionally delegating * response creation to a handler. * - * @param ServerRequestInterface $request - * @param RequestHandlerInterface $handler - * @return ResponseInterface * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function process(ServerRequestInterface $serverRequest, RequestHandlerInterface $handler): ResponseInterface { - if (null === $request->getAttribute('route')) { + if (null === $serverRequest->getAttribute('route')) { throw new HttpNotFoundException('Route not defined'); // @codeCoverageIgnore } - $response = $this->handle($request); + $response = $this->handle($serverRequest); - $otherResponse = $handler->handle($request); + $otherResponse = $handler->handle($serverRequest); $response->getBody()->write($otherResponse->getBody()->getContents()); return $response; @@ -67,18 +49,13 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface /** * Run application middleware. * - * @param ServerRequestInterface $request - * @return ResponseInterface * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ - private function handle(ServerRequestInterface $request): ResponseInterface + private function handle(ServerRequestInterface $serverRequest): ResponseInterface { /** @var array $route */ - $route = $request->getAttribute('route') ?? []; - - //~ Remove route from request - $request = $request->withoutAttribute('route'); + $route = $serverRequest->getAttribute('route') ?? []; [$controllerName, $action] = explode('::', (string) $route['_controller']); @@ -87,22 +64,35 @@ private function handle(ServerRequestInterface $request): ResponseInterface if (!method_exists($controller, $action)) { throw new \DomainException( - 'Action controller does not exists! (' . get_class($controller) . '::' . $action + 'Action controller does not exists! (' . get_class($controller) . '::' . $action, ); } - if ($controller instanceof ControllerInterface) { - //~ Set context action - $controller->setRoute($route); + if (!$controller instanceof ControllerInterface) { + throw new \DomainException('Controller must implement ControllerInterface'); // @codeCoverageIgnore + } + + $controllerParameters = [$serverRequest]; - //~ Call controller pre action, action & post action. - $controller->preAction($request); - $response = $controller->$action($request); - $controller->postAction($request); - } else { - $response = $controller->$action($request); // @codeCoverageIgnore + //~ Add route element to controller parameters to pass it directly. + // /!\ Params from url are always strings + foreach ($route as $key => $value) { + if ($key[0] === '_') { + continue; + } + + $controllerParameters[] = $value; } + //~ Set context action + $controller->setRoute($route); + + //~ Call controller pre action, action & post action. + $controller->preAction($serverRequest); + /** @var ResponseInterface $response */ + $response = $controller->$action(...$controllerParameters); + $controller->postAction($serverRequest); + return $response; } } diff --git a/src/Middleware/ErrorMiddleware.php b/src/Middleware/ErrorMiddleware.php index 4b94db2..2bec662 100644 --- a/src/Middleware/ErrorMiddleware.php +++ b/src/Middleware/ErrorMiddleware.php @@ -12,46 +12,25 @@ namespace Eureka\Kernel\Http\Middleware; use Eureka\Kernel\Http\Controller\ErrorControllerInterface; -use Eureka\Kernel\Http\Exception\HttpInternalServerErrorException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -/** - * Class ErrorMiddleware - * - * @author Romain Cottard - */ -class ErrorMiddleware implements MiddlewareInterface +readonly class ErrorMiddleware implements MiddlewareInterface { - /** @var ErrorControllerInterface $container */ - protected ErrorControllerInterface $controller; - - /** - * ErrorMiddleware constructor. - * - * @param ErrorControllerInterface $errorController - */ - public function __construct(ErrorControllerInterface $errorController) - { - $this->controller = $errorController; - } + public function __construct(private ErrorControllerInterface $controller) {} /** * Process an incoming server request and return a response, optionally delegating * response creation to a handler. - * - * @param ServerRequestInterface $request - * @param RequestHandlerInterface $handler - * @return ResponseInterface */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function process(ServerRequestInterface $serverRequest, RequestHandlerInterface $handler): ResponseInterface { try { - $response = $handler->handle($request); + $response = $handler->handle($serverRequest); } catch (\Throwable $exception) { - $response = $this->getErrorResponse($request, $exception); + $response = $this->getErrorResponse($serverRequest, $exception); } return $response; @@ -59,20 +38,10 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface /** * Get Error response. - * - * @param ServerRequestInterface $serverRequest - * @param \Throwable $exception - * @return ResponseInterface */ private function getErrorResponse(ServerRequestInterface $serverRequest, \Throwable $exception): ResponseInterface { - if ($exception instanceof \Error) { - $exception = new HttpInternalServerErrorException($exception->getMessage(), $exception->getCode(), $exception); - } - - $this->controller->preAction($serverRequest); - /** @var \Exception $exception */ $response = $this->controller->error($serverRequest, $exception); $this->controller->postAction(); diff --git a/src/Middleware/RateLimiterMiddleware.php b/src/Middleware/RateLimiterMiddleware.php index 9b3a556..769151c 100644 --- a/src/Middleware/RateLimiterMiddleware.php +++ b/src/Middleware/RateLimiterMiddleware.php @@ -23,56 +23,38 @@ use Psr\Http\Server\RequestHandlerInterface; /** - * Class RateLimiterMiddleware * Exception Code Range: 910-919 - * - * @author Romain Cottard */ -class RateLimiterMiddleware implements MiddlewareInterface +readonly class RateLimiterMiddleware implements MiddlewareInterface { - protected CacheItemPoolInterface $cache; - protected IpResolver $ipResolver; - - /** - * RateLimiterMiddleware constructor. - * - * @param CacheItemPoolInterface $cache - * @param IpResolver $ipResolver - */ - public function __construct(CacheItemPoolInterface $cache, IpResolver $ipResolver) - { - $this->cache = $cache; - $this->ipResolver = $ipResolver; - } + public function __construct( + private CacheItemPoolInterface $cache, + private IpResolver $ipResolver, + ) {} /** * Process an incoming server request and return a response, optionally delegating * response creation to a handler. * - * @param ServerRequestInterface $request - * @param RequestHandlerInterface $handler - * @return ResponseInterface * @throws HttpTooManyRequestsException */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function process(ServerRequestInterface $serverRequest, RequestHandlerInterface $handler): ResponseInterface { /** @var array|null $route */ - $route = $request->getAttribute('route', null); + $route = $serverRequest->getAttribute('route', null); - if (!empty($route)) { + if (\is_array($route) && $route !== []) { $this->assertQuotaNotReached( $route, - $this->ipResolver->resolve($request) + $this->ipResolver->resolve($serverRequest), ); } - return $handler->handle($request); + return $handler->handle($serverRequest); } /** * @param array $route - * @param string $ip - * @return void * @throws HttpTooManyRequestsException */ private function assertQuotaNotReached(array $route, string $ip): void @@ -80,7 +62,7 @@ private function assertQuotaNotReached(array $route, string $ip): void $quota = (int) ($route['rateLimiterQuota'] ?? 0); $ttl = (int) ($route['rateLimiterTTL'] ?? 0); - if (empty($ttl) || empty($quota)) { + if ($ttl === 0 || $quota === 0) { return; } @@ -95,7 +77,7 @@ private function assertQuotaNotReached(array $route, string $ip): void try { $routeQuotaLimiterProvider->getQuotaLimiter($parameters)->assertQuotaNotReached(); - } catch (QuotaExceededException $exception) { + } catch (QuotaExceededException) { throw new HttpTooManyRequestsException('Too Many Requests', 429); } } diff --git a/src/Middleware/ResponseTimeLoggerMiddleware.php b/src/Middleware/ResponseTimeLoggerMiddleware.php index 7cc167d..e6b4a0f 100644 --- a/src/Middleware/ResponseTimeLoggerMiddleware.php +++ b/src/Middleware/ResponseTimeLoggerMiddleware.php @@ -11,62 +11,68 @@ namespace Eureka\Kernel\Http\Middleware; +use Eureka\Kernel\Http\Exception\HttpInternalServerErrorException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; +use Symfony\Component\Routing\Route; -/** - * Class ResponseTimeLoggerMiddleware - * - * @author Pierre-Olivier Dézard - */ -class ResponseTimeLoggerMiddleware implements MiddlewareInterface +readonly class ResponseTimeLoggerMiddleware implements MiddlewareInterface { - private LoggerInterface $logger; - private string $applicationName; - - /** - * ResponseTimeLoggerMiddleWare constructor. - * - * @param LoggerInterface $logger - * @param string $applicationName - */ public function __construct( - LoggerInterface $logger, - string $applicationName - ) { - $this->logger = $logger; - $this->applicationName = $applicationName; - } + private LoggerInterface $logger, + private string $applicationName, + ) {} /** - * @param ServerRequestInterface $request - * @param RequestHandlerInterface $handler - * @return ResponseInterface + * @throws \Throwable */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function process(ServerRequestInterface $serverRequest, RequestHandlerInterface $handler): ResponseInterface { $time = -microtime(true); - $response = $handler->handle($request); - $time += microtime(true); - $time = (int) ($time * 1000); + try { + $response = $handler->handle($serverRequest); + $httpCode = $response->getStatusCode(); + } catch (\Throwable $exception) { + $httpCode = $exception->getCode() > 599 ? 500 : $exception->getCode(); + } finally { + $time += microtime(true); + $time = (int) ($time * 1000); + + $page = $serverRequest->getUri()->getPath(); + $queryParams = $serverRequest->getQueryParams(); - $page = $request->getUri()->getPath(); + /** @var Route|null $route */ + $route = $serverRequest->getAttribute('routeInstance'); - //In case of a Redirect Response, we don't log the response time because of RouterMiddleware exit - $this->logger->info( - $page . ' took ' . $time . 'ms to respond', - [ - 'type' => $this->applicationName . '.page.response_time', - 'application' => $this->applicationName, - 'page' => $page, - 'counters' => [ - 'page_time_ms' => $time, + //In case of a Redirect Response, we don't log the response time because of RouterMiddleware exit + $this->logger->info( + $page . ' took ' . $time . 'ms to respond', + [ + 'type' => $this->applicationName . '.page.response_time', + 'application' => $this->applicationName, + 'path' => $page, + 'route' => $route?->getPath() ?? '-', + 'queryParams' => $queryParams, + 'httpCode' => $httpCode, + 'counters' => [ + 'page_time_ms' => $time, + ], ], - ] - ); + ); + + //~ Rethrow exception when finally come after a catch of an exception + if (isset($exception)) { + throw $exception; + } + } + + if (!isset($response)) { + //~ Should not happen + throw new HttpInternalServerErrorException('Response not defined!'); // @codeCoverageIgnore + } return $response; } diff --git a/src/Middleware/RouterMiddleware.php b/src/Middleware/RouterMiddleware.php index 0542857..4fcdcad 100644 --- a/src/Middleware/RouterMiddleware.php +++ b/src/Middleware/RouterMiddleware.php @@ -24,42 +24,26 @@ use Symfony\Component\Routing\Router; /** - * Class RouterMiddleware * Exception Code Range: 900-909 - * - * @author Romain Cottard */ -class RouterMiddleware implements MiddlewareInterface +readonly class RouterMiddleware implements MiddlewareInterface { - /** @var Router $router */ - protected Router $router; - - /** - * RouterMiddleware constructor. - * - * @param Router $router - */ - public function __construct(Router $router) - { - $this->router = $router; - } + public function __construct(private Router $router) {} /** * Process an incoming server request and return a response, optionally delegating * response creation to a handler. * - * @param ServerRequestInterface $request - * @param RequestHandlerInterface $handler - * @return ResponseInterface * @throws HttpNotFoundException */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function process(ServerRequestInterface $serverRequest, RequestHandlerInterface $handler): ResponseInterface { //~ Try to match url try { - $this->router->setContext($this->getRequestContext($request)); - $route = $this->router->match($request->getUri()->getPath()); - } catch (ResourceNotFoundException | RouteNotFoundException $exception) { + $this->router->setContext($this->getRequestContext($serverRequest)); + /** @var array $route */ + $route = $this->router->match($serverRequest->getUri()->getPath()); + } catch (ResourceNotFoundException|RouteNotFoundException $exception) { throw new HttpNotFoundException($exception->getMessage(), 900, $exception); } catch (MethodNotAllowedException $exception) { $message = 'Allowed method(s): ' . implode(',', $exception->getAllowedMethods()); @@ -67,7 +51,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface } //~ Add route param to request - $serverRequest = $request->withAttribute('route', $route); + $serverRequest = $serverRequest->withAttribute('route', $route); //~ Add route element to requests foreach ($route as $key => $value) { @@ -78,13 +62,13 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $serverRequest = $serverRequest->withAttribute($key, $value); } + //~ Get symfony route instance & add it to the request + $routeInstance = $this->router->getRouteCollection()->get((string) $route['_route']); // get full route info + $serverRequest = $serverRequest->withAttribute('routeInstance', $routeInstance); + return $handler->handle($serverRequest); } - /** - * @param ServerRequestInterface $serverRequest - * @return RequestContext - */ private function getRequestContext(ServerRequestInterface $serverRequest): RequestContext { $uri = $serverRequest->getUri(); @@ -97,7 +81,7 @@ private function getRequestContext(ServerRequestInterface $serverRequest): Reque 80, 443, $uri->getPath(), - $uri->getQuery() + $uri->getQuery(), ); } } diff --git a/src/RateLimiter/Counter/CacheCounter.php b/src/RateLimiter/Counter/CacheCounter.php index 18c859f..904e5e3 100644 --- a/src/RateLimiter/Counter/CacheCounter.php +++ b/src/RateLimiter/Counter/CacheCounter.php @@ -14,36 +14,20 @@ use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\InvalidArgumentException; -/** - * CacheCounter - * - * @author Romain Cottard - */ class CacheCounter implements CounterInterface { - private CacheItemPoolInterface $cache; - private int $cacheTTL; private int $stepTTL; - /** - * CacheCounter constructor. - * - * @param CacheItemPoolInterface $cache - * @param int $cacheTTL Cache Time to live in second - */ - public function __construct(CacheItemPoolInterface $cache, int $cacheTTL) - { - $this->cache = $cache; - $this->cacheTTL = $cacheTTL; - $this->stepTTL = (int) ceil($cacheTTL / 10); + public function __construct( + private readonly CacheItemPoolInterface $cache, + private readonly int $cacheTTL, + ) { + $this->stepTTL = (int) ceil($cacheTTL / 10); } /** * Increment counter and returns its new value * - * @param string $id - * @param int $value - * @return int * @throws InvalidArgumentException */ public function increment(string $id, int $value = 1): int @@ -67,8 +51,6 @@ public function increment(string $id, int $value = 1): int /** * Returns current counter value * - * @param string $id - * @return int * @throws InvalidArgumentException */ public function current(string $id): int @@ -87,8 +69,6 @@ public function current(string $id): int /** * Deletes a counter * - * @param string $id - * @return void * @throws InvalidArgumentException */ public function delete(string $id): void @@ -100,8 +80,6 @@ public function delete(string $id): void /** * Get Counter Time to live - * - * @return int */ public function getTTL(): int { @@ -110,7 +88,7 @@ public function getTTL(): int /** * @param array $counter - * @return int[] + * @return array */ private function clean(array $counter): array { @@ -126,8 +104,7 @@ private function clean(array $counter): array /** * @param array $counter - * @param int $value - * @return int[] + * @return array */ private function add(array $counter, int $value): array { diff --git a/src/RateLimiter/Counter/CounterInterface.php b/src/RateLimiter/Counter/CounterInterface.php index a213b49..88261ff 100644 --- a/src/RateLimiter/Counter/CounterInterface.php +++ b/src/RateLimiter/Counter/CounterInterface.php @@ -11,42 +11,25 @@ namespace Eureka\Kernel\Http\RateLimiter\Counter; -/** - * Abstract Class CounterInterface - * - * @author Romain Cottard - */ interface CounterInterface { /** * Increment counter and returns its new value - * - * @param string $id - * @param int $value - * @return int */ public function increment(string $id, int $value = 1): int; /** * Returns current counter value - * - * @param string $id - * @return int */ public function current(string $id): int; /** * Deletes a counter - * - * @param string $id - * @return void */ public function delete(string $id): void; /** * Get Counter Time to live - * - * @return int */ public function getTTL(): int; } diff --git a/src/RateLimiter/Exception/QuotaExceededException.php b/src/RateLimiter/Exception/QuotaExceededException.php index 5b20731..493f365 100644 --- a/src/RateLimiter/Exception/QuotaExceededException.php +++ b/src/RateLimiter/Exception/QuotaExceededException.php @@ -11,9 +11,4 @@ namespace Eureka\Kernel\Http\RateLimiter\Exception; -/** - * Exception QuotaExceededException - * - * @author Romain Cottard - */ class QuotaExceededException extends \OutOfBoundsException {} diff --git a/src/RateLimiter/Limiter/LimiterInterface.php b/src/RateLimiter/Limiter/LimiterInterface.php index 6a35451..cb450a0 100644 --- a/src/RateLimiter/Limiter/LimiterInterface.php +++ b/src/RateLimiter/Limiter/LimiterInterface.php @@ -13,17 +13,11 @@ use Eureka\Kernel\Http\RateLimiter\Exception\QuotaExceededException; -/** - * Interface LimiterInterface - * - * @author Romain Cottard - */ interface LimiterInterface { /** * Assert usage is valid or throws exception * - * @return void * @throws QuotaExceededException */ public function assertQuotaNotReached(): void; diff --git a/src/RateLimiter/Limiter/QuotaLimiter.php b/src/RateLimiter/Limiter/QuotaLimiter.php index 16206f2..4ee75bd 100644 --- a/src/RateLimiter/Limiter/QuotaLimiter.php +++ b/src/RateLimiter/Limiter/QuotaLimiter.php @@ -14,28 +14,18 @@ use Eureka\Kernel\Http\RateLimiter\Counter\CounterInterface; use Eureka\Kernel\Http\RateLimiter\Exception\QuotaExceededException; -/** - * Class QuotaLimiter - * - * @author Romain Cottard - */ -class QuotaLimiter implements LimiterInterface +readonly class QuotaLimiter implements LimiterInterface { - private CounterInterface $counters; - private string $counterId; - private int $quota; - /** * @param CounterInterface $counters Auto resetting counter * @param string $counterId * @param int $quota Number of allowed calls during the above time slot */ - public function __construct(CounterInterface $counters, string $counterId, int $quota) - { - $this->counters = $counters; - $this->counterId = $counterId; - $this->quota = $quota; - } + public function __construct( + private CounterInterface $counters, + private string $counterId, + private int $quota, + ) {} /** * Assert usage is valid or throws exception @@ -52,7 +42,7 @@ public function assertQuotaNotReached(): void 'Too many requests. Quota: %d per %d seconds, got %d', $this->quota, $this->counters->getTTL(), - $count + $count, )); } } diff --git a/src/RateLimiter/LimiterProvider/AbstractQuotaLimiterProvider.php b/src/RateLimiter/LimiterProvider/AbstractQuotaLimiterProvider.php index 1208fdb..e8e2384 100644 --- a/src/RateLimiter/LimiterProvider/AbstractQuotaLimiterProvider.php +++ b/src/RateLimiter/LimiterProvider/AbstractQuotaLimiterProvider.php @@ -14,21 +14,12 @@ use Eureka\Kernel\Http\RateLimiter\Counter\CounterInterface; use Eureka\Kernel\Http\RateLimiter\Limiter\QuotaLimiter; -/** - * Abstract Class AbstractQuotaLimiterProvider - * - * @author Romain Cottard - */ abstract class AbstractQuotaLimiterProvider { - private CounterInterface $counter; - private int $quota; - /** * Implement your validation rules here (the mandatory keys in the $parameters with their allowed types). * * @param array $parameters - * @return void * @throws \InvalidArgumentException */ abstract protected function validateParameters(array $parameters): void; @@ -37,25 +28,16 @@ abstract protected function validateParameters(array $parameters): void; * Returns the built counter id from the initial parameters. * * @param array $parameters - * @return string */ abstract protected function buildCounterId(array $parameters): string; - /** - * AbstractQuotaLimiterProvider constructor. - * - * @param CounterInterface $counter - * @param int $quota - */ - public function __construct(CounterInterface $counter, int $quota) - { - $this->counter = $counter; - $this->quota = $quota; - } + public function __construct( + private readonly CounterInterface $counter, + private readonly int $quota, + ) {} /** * @param array $parameters - * @return QuotaLimiter */ public function getQuotaLimiter(array $parameters): QuotaLimiter { @@ -64,7 +46,7 @@ public function getQuotaLimiter(array $parameters): QuotaLimiter return new QuotaLimiter( $this->counter, $this->buildCounterId($parameters), - $this->quota + $this->quota, ); } } diff --git a/src/RateLimiter/LimiterProvider/RouteQuotaLimiterProvider.php b/src/RateLimiter/LimiterProvider/RouteQuotaLimiterProvider.php index 59815b1..2427040 100644 --- a/src/RateLimiter/LimiterProvider/RouteQuotaLimiterProvider.php +++ b/src/RateLimiter/LimiterProvider/RouteQuotaLimiterProvider.php @@ -11,34 +11,25 @@ namespace Eureka\Kernel\Http\RateLimiter\LimiterProvider; -/** - * Class RouteQuotaLimiterProvider - * - * @author Romain Cottard - */ class RouteQuotaLimiterProvider extends AbstractQuotaLimiterProvider { - /** @const PARAM_EMAIL string*/ - public const PARAM_ROUTE = 'route'; - - /** @const PARAM_CLIENT_IP string*/ - public const PARAM_CLIENT_IP = 'ip'; + public const string PARAM_ROUTE = 'route'; + public const string PARAM_CLIENT_IP = 'ip'; /** * Implement your validation rules here (the mandatory keys in the $parameters with their allowed types). * * @param array $parameters - * @return void * @throws \InvalidArgumentException */ protected function validateParameters(array $parameters): void { - if (empty($parameters[self::PARAM_ROUTE]) || !is_string($parameters[self::PARAM_ROUTE])) { + if (!isset($parameters[self::PARAM_ROUTE]) || !\is_string($parameters[self::PARAM_ROUTE])) { throw new \InvalidArgumentException('Parameters should contain an "route" index'); } - if (!isset($parameters[self::PARAM_CLIENT_IP]) || !is_string($parameters[self::PARAM_CLIENT_IP])) { + if (!isset($parameters[self::PARAM_CLIENT_IP]) || !\is_string($parameters[self::PARAM_CLIENT_IP])) { throw new \InvalidArgumentException('Parameters should contain an "ip" index'); } } @@ -47,11 +38,10 @@ protected function validateParameters(array $parameters): void * Returns the built cache key from the initial parameters. * * @param array $parameters - * @return string */ protected function buildCounterId(array $parameters): string { - $hashId = md5($parameters[self::PARAM_ROUTE] . '_' . $parameters[self::PARAM_CLIENT_IP]); + $hashId = \md5($parameters[self::PARAM_ROUTE] . '_' . $parameters[self::PARAM_CLIENT_IP]); return 'rate_limiter.counter.route.' . $hashId; } } diff --git a/src/Service/DataCollection.php b/src/Service/DataCollection.php deleted file mode 100644 index 841597d..0000000 --- a/src/Service/DataCollection.php +++ /dev/null @@ -1,149 +0,0 @@ - - * - * @author Romain Cottard - */ -class DataCollection implements \Iterator -{ - /** @var int $length Length of the collection */ - protected int $length = 0; - - /** @var int Current position of the cursor in collection. */ - protected int $index = 0; - - /** @var array $indices Index of keys */ - protected array $indices = []; - - /** @var array $collection Collection of data. */ - protected array $collection = []; - - /** - * DataCollection constructor. - * - * @param array $data - */ - public function __construct(array $data = []) - { - $this->collection = []; - - if (!empty($data)) { - foreach ($data as $key => $value) { - $this->add((string) $key, $value); - } - } - } - - /** - * Add data to the collection. - * - * @param string $key - * @param mixed $value - * @return $this - */ - public function add(string $key, mixed $value): self - { - $this->collection[$key] = $value; - $this->indices[$this->length] = $key; - $this->length++; - - return $this; - } - - /** - * Get length of the collection. - * - * @return int - */ - public function length(): int - { - return $this->length; - } - - /** - * Get current data - * - * @return mixed - */ - - public function current(): mixed - { - return $this->collection[$this->indices[$this->index]]; - } - - /** - * Reset internal cursor. - * - * @return void - */ - public function reset(): void - { - $this->index = 0; - } - - /** - * Get current key. - * - * @return string - */ - public function key(): string - { - return $this->indices[$this->index]; - } - - /** - * Go to the next data - * - * @return void - */ - public function next(): void - { - $this->index++; - } - - /** - * Go to the previous data. - * - * @return void - */ - public function rewind(): void - { - $this->index = 0; - } - - /** - * Check if have more data in the collection - * - * @return bool - */ - public function valid(): bool - { - return $this->index < $this->length; - } - - /** - * Convert to array - * - * @return array - */ - public function toArray(): array - { - $array = $this->collection; - reset($array); - - return $array; - } -} diff --git a/src/Service/IpResolver.php b/src/Service/IpResolver.php index aa93683..f0048a7 100644 --- a/src/Service/IpResolver.php +++ b/src/Service/IpResolver.php @@ -20,7 +20,7 @@ */ class IpResolver { - private const IP_INDICES_TO_CHECK = [ + private const array IP_INDICES_TO_CHECK = [ 'HTTP_CLIENT_IP', // Shared internet/ISP IP 'HTTP_X_FORWARDED_FOR', // IPs passing through proxies 'HTTP_X_FORWARDED', @@ -34,24 +34,24 @@ class IpResolver * Retrieves the best guess of the client's actual IP address. * Takes into account numerous HTTP proxy headers due to variations * in how different ISPs handle IP addresses in headers between hops. - * - * @param ServerRequestInterface $serverRequest - * @param bool $excludePrivate - * @return string */ public function resolve( ServerRequestInterface $serverRequest, - bool $excludePrivate = false + bool $excludePrivate = false, ): string { + /** @var array $server */ $server = $serverRequest->getServerParams(); foreach (self::IP_INDICES_TO_CHECK as $index) { if (!isset($server[$index])) { continue; } - $ips = $index === 'HTTP_X_FORWARDED_FOR' && isset($server[$index]) ? explode(',', $server[$index]) : [$server[$index]]; + $ips = $index === 'HTTP_X_FORWARDED_FOR' + ? \explode(',', (string) $server[$index]) + : [(string) $server[$index]] + ; foreach ($ips as $ip) { - if (!empty($ip) && $this->validate($ip, $excludePrivate)) { + if ($ip !== '' && $this->validate($ip, $excludePrivate)) { return $ip; } } @@ -60,11 +60,6 @@ public function resolve( return ''; } - /** - * @param string $ip - * @param bool $excludePrivate - * @return bool - */ public function validate(string $ip, bool $excludePrivate = false): bool { $options = FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_RES_RANGE; @@ -73,6 +68,6 @@ public function validate(string $ip, bool $excludePrivate = false): bool $options |= FILTER_FLAG_NO_PRIV_RANGE; } - return (filter_var($ip, FILTER_VALIDATE_IP, $options) !== false); + return \filter_var($ip, FILTER_VALIDATE_IP, $options) !== false; } } diff --git a/src/Traits/HttpFactoryAwareTrait.php b/src/Traits/HttpFactoryAwareTrait.php index 47dd587..aa4d1f9 100644 --- a/src/Traits/HttpFactoryAwareTrait.php +++ b/src/Traits/HttpFactoryAwareTrait.php @@ -17,11 +17,6 @@ use Psr\Http\Message\StreamFactoryInterface; use Psr\Http\Message\UriFactoryInterface; -/** - * Trait HttpFactoryAwareTrait - * - * @author Romain Cottard - */ trait HttpFactoryAwareTrait { private UriFactoryInterface $uriFactory; @@ -30,81 +25,51 @@ trait HttpFactoryAwareTrait private ResponseFactoryInterface $responseFactory; private StreamFactoryInterface $streamFactory; - /** - * @return UriFactoryInterface - */ public function getUriFactory(): UriFactoryInterface { return $this->uriFactory; } - /** - * @param UriFactoryInterface $uriFactory - */ public function setUriFactory(UriFactoryInterface $uriFactory): void { $this->uriFactory = $uriFactory; } - /** - * @return RequestFactoryInterface - */ public function getRequestFactory(): RequestFactoryInterface { return $this->requestFactory; } - /** - * @param RequestFactoryInterface $requestFactory - */ public function setRequestFactory(RequestFactoryInterface $requestFactory): void { $this->requestFactory = $requestFactory; } - /** - * @return ServerRequestFactoryInterface - */ public function getServerRequestFactory(): ServerRequestFactoryInterface { return $this->serverRequestFactory; } - /** - * @param ServerRequestFactoryInterface $serverRequestFactory - */ public function setServerRequestFactory(ServerRequestFactoryInterface $serverRequestFactory): void { $this->serverRequestFactory = $serverRequestFactory; } - /** - * @return ResponseFactoryInterface - */ public function getResponseFactory(): ResponseFactoryInterface { return $this->responseFactory; } - /** - * @param ResponseFactoryInterface $responseFactory - */ public function setResponseFactory(ResponseFactoryInterface $responseFactory): void { $this->responseFactory = $responseFactory; } - /** - * @return StreamFactoryInterface - */ public function getStreamFactory(): StreamFactoryInterface { return $this->streamFactory; } - /** - * @param StreamFactoryInterface $streamFactory - */ public function setStreamFactory(StreamFactoryInterface $streamFactory): void { $this->streamFactory = $streamFactory; diff --git a/src/Traits/LoggerAwareTrait.php b/src/Traits/LoggerAwareTrait.php index 906ff6e..9ab292a 100644 --- a/src/Traits/LoggerAwareTrait.php +++ b/src/Traits/LoggerAwareTrait.php @@ -13,28 +13,15 @@ use Psr\Log\LoggerInterface; -/** - * Trait LoggerAwareTrait - * - * @author Romain Cottard - */ trait LoggerAwareTrait { protected LoggerInterface $logger; - /** - * Sets a logger. - * - * @param LoggerInterface $logger - */ public function setLogger(LoggerInterface $logger): void { $this->logger = $logger; } - /** - * @return LoggerInterface - */ protected function getLogger(): LoggerInterface { return $this->logger; diff --git a/src/Traits/RouterAwareTrait.php b/src/Traits/RouterAwareTrait.php index 82f96d7..b7eb2a1 100644 --- a/src/Traits/RouterAwareTrait.php +++ b/src/Traits/RouterAwareTrait.php @@ -13,11 +13,6 @@ use Symfony\Component\Routing\Router; -/** - * Trait RouterAwareTrait - * - * @author Romain Cottard - */ trait RouterAwareTrait { protected Router $router; @@ -25,10 +20,6 @@ trait RouterAwareTrait /** @var array */ protected array $route = []; - /** - * @param Router $router - * @return void - */ public function setRouter(Router $router): void { $this->router = $router; @@ -36,16 +27,12 @@ public function setRouter(Router $router): void /** * @param array $route - * @return void */ public function setRoute(array $route): void { $this->route = $route; } - /** - * @return Router - */ protected function getRouter(): Router { return $this->router; @@ -60,30 +47,22 @@ protected function getRoute(): array } /** - * @param string $routeName * @param array $params - * @return string */ protected function getRouteUri(string $routeName, array $params = []): string { return $this->router->generate($routeName, $params); } - /** - * @param string $name - * @return bool - */ protected function hasParameter(string $name): bool { return isset($this->route[$name]); } /** - * @param string $name * @param string|int|bool|float|null $default - * @return mixed|null */ - protected function getParameter(string $name, $default = null) + protected function getParameter(string $name, mixed $default = null): mixed { return $this->route[$name] ?? $default; } diff --git a/src/Traits/ServerRequestAwareTrait.php b/src/Traits/ServerRequestAwareTrait.php index 2631ddd..d7badc9 100644 --- a/src/Traits/ServerRequestAwareTrait.php +++ b/src/Traits/ServerRequestAwareTrait.php @@ -11,127 +11,54 @@ namespace Eureka\Kernel\Http\Traits; -use Eureka\Kernel\Http\Service\DataCollection; use Psr\Http\Message\ServerRequestInterface; -/** - * Trait ServerRequestAwareTrait - * - * @author Romain Cottard - */ trait ServerRequestAwareTrait { - /** @var ServerRequestInterface $serverRequest */ protected ServerRequestInterface $serverRequest; - /** - * @param ServerRequestInterface $serverRequest - * @return void - */ public function setServerRequest(ServerRequestInterface $serverRequest): void { $this->serverRequest = $serverRequest; } - /** - * @return ServerRequestInterface - */ protected function getServerRequest(): ServerRequestInterface { return $this->serverRequest; } - /** - * @return DataCollection - */ - protected function getQueryParameters(): DataCollection - { - return new DataCollection($this->serverRequest->getQueryParams()); - } - - /** - * @return DataCollection - */ - protected function getBodyParameters(): DataCollection - { - return new DataCollection((array) $this->serverRequest->getParsedBody()); - } - - /** - * @return bool - */ - protected function isHttpGetMethod(): bool - { - return (strtoupper($this->serverRequest->getMethod()) === 'GET'); - } - - /** - * @return bool - */ - protected function isHttpPutMethod(): bool - { - return (strtoupper($this->serverRequest->getMethod()) === 'PUT'); - } - - /** - * @return bool - */ - protected function isHttpPatchMethod(): bool - { - return (strtoupper($this->serverRequest->getMethod()) === 'PATCH'); - } - - /** - * @return bool - */ - protected function isHttpPostMethod(): bool - { - return (strtoupper($this->serverRequest->getMethod()) === 'POST'); - } - - /** - * @return bool - */ - protected function isHttpDeleteMethod(): bool + public function isHttpMethod(string $method): bool { - return (strtoupper($this->serverRequest->getMethod()) === 'DELETE'); + return \strtoupper($this->serverRequest->getMethod()) === \strtoupper($method); } - /** - * @return bool - */ - protected function isAjax(): bool + protected function isAjaxRequest(): bool { $server = $this->serverRequest->getServerParams(); - if (empty($server['HTTP_X_REQUESTED_WITH'])) { + $requestedWith = $server['HTTP_X_REQUESTED_WITH'] ?? ''; + if (!\is_string($requestedWith) || $requestedWith === '') { return false; } - return (strtolower($server['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'); + return \strtolower($requestedWith) === 'xmlhttprequest'; } - /** - * @return bool - */ protected function isJsonRequest(): bool { if (!$this->serverRequest->hasHeader('Content-Type')) { return false; } - return (strtolower($this->serverRequest->getHeaderLine('Content-Type')) === 'application/json'); + return \strtolower($this->serverRequest->getHeaderLine('Content-Type')) === 'application/json'; } - /** - * @return bool - */ protected function acceptJsonResponse(): bool { if (!$this->serverRequest->hasHeader('Accept')) { return false; } - return (strtolower($this->serverRequest->getHeaderLine('Accept')) === 'application/json'); + return \strtolower($this->serverRequest->getHeaderLine('Accept')) === 'application/json'; } } diff --git a/tests/Integration/.gitkeep b/tests/Integration/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/ApplicationTest.php b/tests/Unit/ApplicationTest.php similarity index 71% rename from tests/unit/ApplicationTest.php rename to tests/Unit/ApplicationTest.php index 1b58bab..4d138b2 100644 --- a/tests/unit/ApplicationTest.php +++ b/tests/Unit/ApplicationTest.php @@ -21,32 +21,37 @@ use Eureka\Kernel\Http\Exception\HttpTooManyRequestsException; use Eureka\Kernel\Http\Exception\HttpUnauthorizedException; use Eureka\Kernel\Http\Kernel; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -/** - * Class ApplicationTest - * - * @author Romain Cottard - */ class ApplicationTest extends TestCase { /** - * @return void * @throws \Exception */ - public function testCanInstantiateApplication(): void + public function testCanRunApplicationWithJsonResponse(): void { - self::assertInstanceOf(ApplicationInterface::class, $this->getApplication()); + //~ Define current route + $_SERVER['REQUEST_URI'] = '/test/json'; + $_SERVER['REQUEST_METHOD'] = 'GET'; + + //~ Run Application + ob_start(); + $application = $this->getApplication(); + $application->send($application->run()); + $output = ob_get_clean(); + + self::assertSame('"ok"', $output); } /** - * @return void * @throws \Exception */ - public function testCanRunApplicationWithJsonResponse(): void + public function testCanRunApplicationWithHtmlResponse(): void { //~ Define current route - $_SERVER['REQUEST_URI'] = '/test/json'; + $_SERVER['REQUEST_URI'] = '/test/html'; + $_SERVER['REQUEST_METHOD'] = 'GET'; //~ Run Application ob_start(); @@ -54,17 +59,17 @@ public function testCanRunApplicationWithJsonResponse(): void $application->send($application->run()); $output = ob_get_clean(); - self::assertSame('"ok"', $output); + self::assertSame('ok', $output); } /** - * @return void * @throws \Exception */ - public function testCanRunApplicationWithHtmlResponse(): void + public function testCanRunApplicationWithUrlParameters(): void { //~ Define current route - $_SERVER['REQUEST_URI'] = '/test/html'; + $_SERVER['REQUEST_URI'] = '/test/entities/1/super-title'; + $_SERVER['REQUEST_METHOD'] = 'GET'; //~ Run Application ob_start(); @@ -72,17 +77,17 @@ public function testCanRunApplicationWithHtmlResponse(): void $application->send($application->run()); $output = ob_get_clean(); - self::assertSame('ok', $output); + self::assertSame('entity id: 1, title: super-title, someString: value, someBool: true, someInt: 42', $output); } /** - * @return void * @throws \Exception */ public function testCanRunApplicationWithRouteNotFoundResponse(): void { //~ Define current route - $_SERVER['REQUEST_URI'] = '/test/error/not-found'; + $_SERVER['REQUEST_URI'] = '/test/error/not-found'; + $_SERVER['REQUEST_METHOD'] = 'GET'; //~ Run Application ob_start(); @@ -91,17 +96,17 @@ public function testCanRunApplicationWithRouteNotFoundResponse(): void $output = ob_get_clean(); $expected = "
exception[Eureka\Kernel\Http\Exception\HttpNotFoundException]: \nNo routes found for \"/test/error/not-found\".\n\n
"; - self::assertEquals($expected, $output); + self::assertSame($expected, $output); } /** - * @return void * @throws \Exception */ public function testCanRunApplicationWhichGenerateTooManyRequestsWhenQuotaIsReach(): void { //~ Define current route - $_SERVER['REQUEST_URI'] = '/test/json/limited'; + $_SERVER['REQUEST_URI'] = '/test/json/limited'; + $_SERVER['REQUEST_METHOD'] = 'GET'; //~ Run Application ob_start(); @@ -116,11 +121,10 @@ public function testCanRunApplicationWhichGenerateTooManyRequestsWhenQuotaIsReac $output = ob_get_clean(); $expected = "
exception[" . HttpTooManyRequestsException::class . "]: \nToo Many Requests\n\n
"; - self::assertEquals($expected, $output); + self::assertSame($expected, $output); } /** - * @return void * @throws \Exception */ public function testCanRunApplicationWhichGenerateAppropriateErrorResponseForNotAllowedMethod(): void @@ -136,23 +140,40 @@ public function testCanRunApplicationWhichGenerateAppropriateErrorResponseForNot $output = ob_get_clean(); $expected = "
exception[Eureka\Kernel\Http\Exception\HttpMethodNotAllowedException]: \nAllowed method(s): GET\n\n
"; - self::assertEquals($expected, $output); + self::assertSame($expected, $output); } /** - * @param string $uri - * @param string $exceptionClass - * @return void * @throws \Exception - * - * @dataProvider uriExceptionDataProvider */ + public function testCanRunApplicationAndGetErrorResponseWhenNonCaughtErrorIsThrown(): void + { + //~ Define current route + $_SERVER['REQUEST_URI'] = '/test/error/html/internal-server-error'; + $_SERVER['REQUEST_METHOD'] = 'POST'; + + $application = new Application(new Kernel((string) realpath(__DIR__ . '/../..'), 'test', true)); + + //~ Run Application + ob_start(); + $application->send($application->run()); + $output = ob_get_clean(); + + $expected = "
exception[Eureka\Kernel\Http\Exception\HttpMethodNotAllowedException]: \nAllowed method(s): GET\n\n
"; + self::assertSame($expected, $output); + } + + /** + * @throws \Exception + */ + #[DataProvider('uriExceptionDataProvider')] public function testCanRunApplicationWhichGenerateAppropriateErrorResponseForGivenError(string $uri, string $exceptionClass): void { //~ Define current route - $_SERVER['REQUEST_URI'] = $uri; - $_SERVER['SERVER_NAME'] = 'any'; - $_SERVER['HTTP_ACCEPT'] = 'application/json'; + $_SERVER['REQUEST_URI'] = $uri; + $_SERVER['SERVER_NAME'] = 'any'; + $_SERVER['HTTP_ACCEPT'] = 'application/json'; + $_SERVER['REQUEST_METHOD'] = 'GET'; //~ Run Application ob_start(); @@ -160,12 +181,11 @@ public function testCanRunApplicationWhichGenerateAppropriateErrorResponseForGiv $application->send($application->run()); $output = ob_get_clean(); - $expected = "
exception[${exceptionClass}]: \nthrow an error (html)\n\n
"; - self::assertEquals($expected, $output); + $expected = "
exception[{$exceptionClass}]: \nthrow an error (html)\n\n
"; + self::assertSame($expected, $output); } /** - * @return void * @throws \Exception */ public function testCanRunApplicationWhichGenerateAppropriateErrorResponseForNotExistingActionMethod(): void @@ -176,7 +196,8 @@ public function testCanRunApplicationWhichGenerateAppropriateErrorResponseForNot $_SERVER['SERVER_PORT'] = 443; $_SERVER['QUERY_STRING'] = 'foo=bar'; - $_SERVER['REQUEST_URI'] = '/test/error/action-not-exists'; + $_SERVER['REQUEST_URI'] = '/test/error/action-not-exists'; + $_SERVER['REQUEST_METHOD'] = 'GET'; //~ Run Application ob_start(); @@ -185,11 +206,10 @@ public function testCanRunApplicationWhichGenerateAppropriateErrorResponseForNot $output = ob_get_clean(); $expected = "
exception[DomainException]: \nAction controller does not exists! (Eureka\Kernel\Http\Tests\Unit\Mock\TestController::testErrorHtmlActionNotExists\n\n
"; - self::assertEquals($expected, $output); + self::assertSame($expected, $output); } /** - * @return ApplicationInterface * @throws \Exception */ private function getApplication(): ApplicationInterface @@ -233,7 +253,7 @@ public static function uriExceptionDataProvider(): array ], 'TypeError' => [ '/test/error/html/type-error', - HttpInternalServerErrorException::class, + \TypeError::class, ], ]; } diff --git a/tests/unit/ControllerTest.php b/tests/Unit/ControllerTest.php similarity index 90% rename from tests/unit/ControllerTest.php rename to tests/Unit/ControllerTest.php index 3c2347a..d112864 100644 --- a/tests/unit/ControllerTest.php +++ b/tests/Unit/ControllerTest.php @@ -11,30 +11,24 @@ namespace Eureka\Kernel\Http\Tests\Unit; -use Eureka\Kernel\Http\Controller\ControllerInterface; use Eureka\Kernel\Http\Kernel; use Eureka\Kernel\Http\Tests\Unit\Mock\TestController; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestFactoryInterface; -/** - * Class ControllerTest - * - * @author Romain Cottard - */ class ControllerTest extends TestCase { /** - * @return void * @throws \Exception */ public function testKernelCanAutowireAController(): void { - self::assertInstanceOf(ControllerInterface::class, $this->getTestController()); + $this->getTestController(); + + $this->expectNotToPerformAssertions(); } /** - * @return void * @throws \Exception */ public function testControllerTraitHttpFactories(): void @@ -45,7 +39,6 @@ public function testControllerTraitHttpFactories(): void } /** - * @return void * @throws \Exception */ public function testControllerHasLogger(): void @@ -56,7 +49,6 @@ public function testControllerHasLogger(): void } /** - * @return void * @throws \Exception */ public function testControllerHasRoutingHelperAvailable(): void @@ -67,7 +59,6 @@ public function testControllerHasRoutingHelperAvailable(): void } /** - * @return void * @throws \Exception */ public function testControllerHasServerRequestHelperAvailable(): void @@ -87,10 +78,10 @@ public function testControllerHasServerRequestHelperAvailable(): void ; self::assertTrue($controller->assertHasServerRequestHelperAvailable($serverRequest)); + self::assertTrue($controller->assertIsAjaxRequest($serverRequest)); } /** - * @return void * @throws \Exception */ public function testControllerAbstractMethods(): void @@ -102,7 +93,6 @@ public function testControllerAbstractMethods(): void } /** - * @return void * @throws \Exception */ public function testICanCheckWhenRequestIsNotJsonNorAjax(): void @@ -117,7 +107,6 @@ public function testICanCheckWhenRequestIsNotJsonNorAjax(): void } /** - * @return TestController * @throws \Exception */ private function getTestController(): TestController @@ -132,7 +121,6 @@ private function getTestController(): TestController } /** - * @return Kernel * @throws \Exception */ private function getKernel(): Kernel diff --git a/tests/unit/KernelTest.php b/tests/Unit/KernelTest.php similarity index 67% rename from tests/unit/KernelTest.php rename to tests/Unit/KernelTest.php index 4565d38..6a4aa0e 100644 --- a/tests/unit/KernelTest.php +++ b/tests/Unit/KernelTest.php @@ -13,17 +13,10 @@ use Eureka\Kernel\Http\Kernel; use PHPUnit\Framework\TestCase; -use Psr\Container\ContainerInterface; -/** - * Class KernelTest - * - * @author Romain Cottard - */ class KernelTest extends TestCase { /** - * @return void * @throws \Exception */ public function testCanInstantiateKernel(): void @@ -32,13 +25,12 @@ public function testCanInstantiateKernel(): void $env = 'dev'; $debug = true; - $kernel = new Kernel($root, $env, $debug); + new Kernel($root, $env, $debug); - self::assertInstanceOf(Kernel::class, $kernel); + $this->expectNotToPerformAssertions(); } /** - * @return void * @throws \Exception */ public function testCanGetContainer(): void @@ -47,8 +39,8 @@ public function testCanGetContainer(): void $env = 'dev'; $debug = true; - $kernel = new Kernel($root, $env, $debug); + new Kernel($root, $env, $debug); - self::assertInstanceOf(ContainerInterface::class, $kernel->getContainer()); + $this->expectNotToPerformAssertions(); } } diff --git a/tests/unit/Mock/TestController.php b/tests/Unit/Mock/TestController.php similarity index 56% rename from tests/unit/Mock/TestController.php rename to tests/Unit/Mock/TestController.php index 08cd83a..fb8ebfb 100644 --- a/tests/unit/Mock/TestController.php +++ b/tests/Unit/Mock/TestController.php @@ -18,146 +18,90 @@ use Eureka\Kernel\Http\Exception\HttpInternalServerErrorException; use Eureka\Kernel\Http\Exception\HttpServiceUnavailableException; use Eureka\Kernel\Http\Exception\HttpUnauthorizedException; -use Eureka\Kernel\Http\Service\DataCollection; -use Psr\Http\Message\RequestFactoryInterface; -use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestFactoryInterface; use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Message\StreamFactoryInterface; -use Psr\Http\Message\UriFactoryInterface; -use Psr\Log\LoggerInterface; -use Symfony\Component\Routing\RouterInterface; -/** - * Controller class - * - * @author Romain Cottard - */ class TestController extends Controller { - private const EXCEPTION_MESSAGE = 'throw an error (html)'; + private const string EXCEPTION_MESSAGE = 'throw an error (html)'; - /** - * @return ResponseInterface - */ public function testJsonAction(): ResponseInterface { return $this->getResponseJson('ok'); } - /** - * @return ResponseInterface - */ public function testHtmlAction(): ResponseInterface { return $this->getResponse('ok'); } - /** - * @return ResponseInterface - */ + public function testUrlAction( + ServerRequestInterface $serverRequest, + string $someString, + bool $someBool, + int $someInt, + string $id, + string $title, + ): ResponseInterface { + return $this->getResponse("entity id: $id, title: $title, someString: $someString, someBool: " . ($someBool ? 'true' : 'false') . ", someInt: $someInt"); + } + public function testInternalServerErrorHtmlAction(): ResponseInterface { throw new HttpInternalServerErrorException(self::EXCEPTION_MESSAGE, 99); } - /** - * @return ResponseInterface - */ public function testBadRequestErrorHtmlAction(): ResponseInterface { throw new HttpBadRequestException(self::EXCEPTION_MESSAGE, 99); } - /** - * @return ResponseInterface - */ public function testUnauthorizedErrorHtmlAction(): ResponseInterface { throw new HttpUnauthorizedException(self::EXCEPTION_MESSAGE, 99); } - /** - * @return ResponseInterface - */ public function testForbiddenErrorHtmlAction(): ResponseInterface { throw new HttpForbiddenException(self::EXCEPTION_MESSAGE, 99); } - /** - * @return ResponseInterface - */ public function testServiceUnavailableErrorHtmlAction(): ResponseInterface { throw new HttpServiceUnavailableException(self::EXCEPTION_MESSAGE, 99); } - /** - * @return ResponseInterface - */ public function testConflictErrorHtmlAction(): ResponseInterface { throw new HttpConflictException(self::EXCEPTION_MESSAGE, 99); } - /** - * @return ResponseInterface - */ public function testTypeErrorHtmlAction(): ResponseInterface { throw new \TypeError(self::EXCEPTION_MESSAGE, 99); } - /** - * @return bool - */ public function assertHasAllFactories(): bool { - if (!($this->getServerRequestFactory() instanceof ServerRequestFactoryInterface)) { - throw new \RuntimeException('ServerRequest Factory not available!'); - } - - if (!($this->getResponseFactory() instanceof ResponseFactoryInterface)) { - throw new \RuntimeException('Response Factory not available!'); - } - - if (!($this->getStreamFactory() instanceof StreamFactoryInterface)) { - throw new \RuntimeException('Stream Factory not available!'); - } - - if (!($this->getUriFactory() instanceof UriFactoryInterface)) { - throw new \RuntimeException('Uri Factory not available!'); - } - - if (!($this->getRequestFactory() instanceof RequestFactoryInterface)) { - throw new \RuntimeException('Request Factory not available!'); - } + $serverRequestFactory = $this->getServerRequestFactory(); + $responseFactory = $this->getResponseFactory(); + $streamFactory = $this->getStreamFactory(); + $uriFactory = $this->getUriFactory(); + $requestFactory = $this->getRequestFactory(); return true; } - /** - * @return bool - */ public function assertHasLogger(): bool { - if (!($this->getLogger() instanceof LoggerInterface)) { - throw new \RuntimeException('Request Factory not available!'); - } + $logger = $this->getLogger(); return true; } - /** - * @return bool - */ public function assertHasRoutingHelperAvailable(): bool { - if (!($this->getRouter() instanceof RouterInterface)) { - throw new \RuntimeException('Router not available!'); - } + $router = $this->getRouter(); //~ Not defined when controller is not called from middleware, so just call to check method availability $route = $this->getRoute(); @@ -177,87 +121,57 @@ public function assertHasRoutingHelperAvailable(): bool return true; } - /** - * @param ServerRequestInterface $serverRequest - * @return bool - */ public function assertHasServerRequestHelperAvailable(ServerRequestInterface $serverRequest): bool { $this->setServerRequest($serverRequest); - if (!($this->getServerRequest() instanceof ServerRequestInterface)) { - throw new \RuntimeException('Invalid server request!'); - } - - if (!($this->getQueryParameters() instanceof DataCollection)) { - throw new \RuntimeException('Invalid query parameters server request!'); - } - - if (!($this->getBodyParameters() instanceof DataCollection)) { - throw new \RuntimeException('Invalid body parameters from server request!'); - } - - if ($this->isHttpGetMethod() !== false) { - throw new \RuntimeException('Invalid Http method GET! Should be a POST method!'); - } - - if ($this->isHttpPatchMethod() !== false) { - throw new \RuntimeException('Invalid Http method PATCH! Should be a POST method!'); - } + $serverRequest = $this->getServerRequest(); - if ($this->isHttpPutMethod() !== false) { - throw new \RuntimeException('Invalid Http method PUT! Should be a POST method!'); + if ($this->isHttpMethod('POST') !== true) { + throw new \RuntimeException('Invalid Http method! Should be a POST method!'); } - if ($this->isHttpDeleteMethod() !== false) { - throw new \RuntimeException('Invalid Http method DELETE! Should be a POST method!'); - } + return true; + } - if ($this->isHttpPostMethod() !== true) { - throw new \RuntimeException('Invalid Http method! Should be a POST method!'); - } + public function assertIsNotJsonNorAjaxRequest(ServerRequestInterface $serverRequest): bool + { + $this->setServerRequest($serverRequest); - if ($this->isAjax() === false) { - throw new \RuntimeException('Invalid request. Should be an ajax request!'); + if ($this->isJsonRequest() === true) { + throw new \RuntimeException('Invalid request. Should not be a json request!'); } - if ($this->isJsonRequest() === false) { - throw new \RuntimeException('Invalid request. Should be a json request!'); + if ($this->acceptJsonResponse() === true) { + throw new \RuntimeException('Invalid request. Should not be accept json response!'); } - if ($this->acceptJsonResponse() === false) { - throw new \RuntimeException('Invalid request. Should be accept json response!'); + if ($this->isAjaxRequest() === true) { + throw new \RuntimeException('Invalid request. Should not be an ajax request!'); } return true; } - /** - * @param ServerRequestInterface $serverRequest - * @return bool - */ - public function assertIsNotJsonNorAjaxRequest(ServerRequestInterface $serverRequest): bool + public function assertIsAjaxRequest(ServerRequestInterface $serverRequest): bool { $this->setServerRequest($serverRequest); - if ($this->isJsonRequest() === true) { - throw new \RuntimeException('Invalid request. Should not be a json request!'); + if ($this->isJsonRequest() !== true) { + throw new \RuntimeException('Invalid request. Should be a json request!'); } - if ($this->acceptJsonResponse() === true) { - throw new \RuntimeException('Invalid request. Should not be accept json response!'); + if ($this->acceptJsonResponse() !== true) { + throw new \RuntimeException('Invalid request. Should accept json response!'); } - if ($this->isAjax() === true) { - throw new \RuntimeException('Invalid request. Should not be an ajax request!'); + if ($this->isAjaxRequest() !== true) { + throw new \RuntimeException('Invalid request. Should be an ajax request!'); } return true; } - /** - * @return bool - */ public function assertHasPropertiesCorrectlySet(): bool { if ($this->isDev() === false) { diff --git a/tests/unit/RateLimiter/CacheCounterTest.php b/tests/Unit/RateLimiter/CacheCounterTest.php similarity index 68% rename from tests/unit/RateLimiter/CacheCounterTest.php rename to tests/Unit/RateLimiter/CacheCounterTest.php index fbc6375..0a5b8ea 100644 --- a/tests/unit/RateLimiter/CacheCounterTest.php +++ b/tests/Unit/RateLimiter/CacheCounterTest.php @@ -16,28 +16,12 @@ use Psr\Cache\InvalidArgumentException; use Symfony\Component\Cache\Adapter\ArrayAdapter; -/** - * Class CacheCounterTest - * - * @author Romain Cottard - */ class CacheCounterTest extends TestCase { - /** @var string COUNTER_ID */ - private const COUNTER_ID = 'counter.id'; - - /** - * @return void - */ - public function testICanInstantiateCacheCounterClass(): void - { - $cacheCounter = new CacheCounter(new ArrayAdapter(100), 5); - - self::assertInstanceOf(CacheCounter::class, $cacheCounter); - } + private const string COUNTER_ID = 'counter.id'; /** - * @return void + * @throws InvalidArgumentException */ public function testICanAddValueOneTwiceAndGetTwoAsValue(): void { @@ -45,11 +29,10 @@ public function testICanAddValueOneTwiceAndGetTwoAsValue(): void $cacheCounter->increment(self::COUNTER_ID, 1); $cacheCounter->increment(self::COUNTER_ID, 1); - self::assertEquals(2, $cacheCounter->current(self::COUNTER_ID)); + self::assertSame(2, $cacheCounter->current(self::COUNTER_ID)); } /** - * @return void * @throws InvalidArgumentException */ public function testICanAddValueOneTwiceAndGetOneAsValueWhenFirstElementIsOutOfTTL(): void @@ -59,11 +42,10 @@ public function testICanAddValueOneTwiceAndGetOneAsValueWhenFirstElementIsOutOfT sleep(2); $cacheCounter->increment(self::COUNTER_ID, 1); - self::assertEquals(1, $cacheCounter->current(self::COUNTER_ID)); + self::assertSame(1, $cacheCounter->current(self::COUNTER_ID)); } /** - * @return void * @throws InvalidArgumentException */ public function testICanAddValueOneTwiceAndGetZeroAsValueWhenAllElementsAreOutOfTTL(): void @@ -73,11 +55,10 @@ public function testICanAddValueOneTwiceAndGetZeroAsValueWhenAllElementsAreOutOf $cacheCounter->increment(self::COUNTER_ID, 1); sleep(2); - self::assertEquals(0, $cacheCounter->current(self::COUNTER_ID)); + self::assertSame(0, $cacheCounter->current(self::COUNTER_ID)); } /** - * @return void * @throws InvalidArgumentException */ public function testICanAddValueOneTwiceAndGetZeroAfterDeletionOfCounter(): void @@ -86,20 +67,17 @@ public function testICanAddValueOneTwiceAndGetZeroAfterDeletionOfCounter(): void $cacheCounter->increment(self::COUNTER_ID, 1); $cacheCounter->increment(self::COUNTER_ID, 1); - self::assertEquals(2, $cacheCounter->current(self::COUNTER_ID)); + self::assertSame(2, $cacheCounter->current(self::COUNTER_ID)); $cacheCounter->delete(self::COUNTER_ID); - self::assertEquals(0, $cacheCounter->current(self::COUNTER_ID)); + self::assertSame(0, $cacheCounter->current(self::COUNTER_ID)); } - /** - * @return void - */ public function testICanGetCounterTTLValue(): void { $cacheCounter = new CacheCounter(new ArrayAdapter(100), 10); - self::assertEquals(10, $cacheCounter->getTTL()); + self::assertSame(10, $cacheCounter->getTTL()); } } diff --git a/tests/unit/RateLimiter/LimiterProviderTest.php b/tests/Unit/RateLimiter/LimiterProviderTest.php similarity index 83% rename from tests/unit/RateLimiter/LimiterProviderTest.php rename to tests/Unit/RateLimiter/LimiterProviderTest.php index b998ca3..e72ed05 100644 --- a/tests/unit/RateLimiter/LimiterProviderTest.php +++ b/tests/Unit/RateLimiter/LimiterProviderTest.php @@ -17,29 +17,10 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Cache\Adapter\ArrayAdapter; -/** - * Class LimiterProviderTest - * - * @author Romain Cottard - */ class LimiterProviderTest extends TestCase { - private const LOCAL_IP = '127.0.0.1'; - - /** - * @return void - */ - public function testICanInstantiateRouteQuotaLimiterProviderClass(): void - { - $cacheCounter = new CacheCounter(new ArrayAdapter(100), 5); - $limiterProvider = new RouteQuotaLimiterProvider($cacheCounter, 2); - - self::assertInstanceOf(RouteQuotaLimiterProvider::class, $limiterProvider); - } + private const string LOCAL_IP = '127.0.0.1'; - /** - * @return void - */ public function testICanAssertTwiceQuotaIsNotReachedWithTwoAsQuota(): void { $cacheCounter = new CacheCounter(new ArrayAdapter(100), 5); @@ -53,12 +34,9 @@ public function testICanAssertTwiceQuotaIsNotReachedWithTwoAsQuota(): void $limiterProvider->getQuotaLimiter($parameters)->assertQuotaNotReached(); $limiterProvider->getQuotaLimiter($parameters)->assertQuotaNotReached(); - self::assertTrue(true); + $this->expectNotToPerformAssertions(); } - /** - * @return void - */ public function testAnExceptionIsThrownWhenTryToAssertTriceWithTwoAsQuota(): void { $cacheCounter = new CacheCounter(new ArrayAdapter(100), 5); @@ -77,9 +55,6 @@ public function testAnExceptionIsThrownWhenTryToAssertTriceWithTwoAsQuota(): voi $limiterProvider->getQuotaLimiter($parameters)->assertQuotaNotReached(); } - /** - * @return void - */ public function testAnExceptionIsThrownWhenTryToGetQuotaLimiterWithoutRequiredRouteParameters(): void { $cacheCounter = new CacheCounter(new ArrayAdapter(100), 5); @@ -95,9 +70,6 @@ public function testAnExceptionIsThrownWhenTryToGetQuotaLimiterWithoutRequiredRo $limiterProvider->getQuotaLimiter($parameters); } - /** - * @return void - */ public function testAnExceptionIsThrownWhenTryToGetQuotaLimiterWithoutRequiredIpParameters(): void { $cacheCounter = new CacheCounter(new ArrayAdapter(100), 5); diff --git a/tests/unit/Service/IpResolverTest.php b/tests/Unit/Service/IpResolverTest.php similarity index 69% rename from tests/unit/Service/IpResolverTest.php rename to tests/Unit/Service/IpResolverTest.php index 6dbf217..044c33e 100644 --- a/tests/unit/Service/IpResolverTest.php +++ b/tests/Unit/Service/IpResolverTest.php @@ -16,16 +16,8 @@ use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface; -/** - * Class IpTest - * - * @author Romain Cottard - */ class IpResolverTest extends TestCase { - /** - * @return void - */ public function testIGetEmptyIpFromUtilsWhenUseLocalhostIp(): void { $serverRequest = $this->getServerRequest('127.0.0.1'); @@ -33,29 +25,27 @@ public function testIGetEmptyIpFromUtilsWhenUseLocalhostIp(): void self::assertEmpty((new IpResolver())->resolve($serverRequest)); } - /** - * @return void - */ public function testIGetMyIpFromUtilsWhenUseMyIp(): void { $serverRequest = $this->getServerRequest('1.2.3.4'); - self::assertEquals('1.2.3.4', (new IpResolver())->resolve($serverRequest)); + self::assertSame('1.2.3.4', (new IpResolver())->resolve($serverRequest)); + } + + public function testIGetMyIpFromUtilsWhenUseMyIpWithXForwardedForIps(): void + { + $serverRequest = $this->getServerRequest('1.2.3.4', '1.2.3.5,1.2.3.6'); + + self::assertSame('1.2.3.5', (new IpResolver())->resolve($serverRequest)); } - /** - * @return void - */ public function testIGetMyPrivateIpFromUtilsWhenUseMyPrivateIp(): void { $serverRequest = $this->getServerRequest('172.16.1.2'); - self::assertEquals('172.16.1.2', (new IpResolver())->resolve($serverRequest)); + self::assertSame('172.16.1.2', (new IpResolver())->resolve($serverRequest)); } - /** - * @return void - */ public function testIGetEmptyIpFromUtilsWithExcludedPrivateIpWhenUseMyPrivateIp(): void { $serverRequest = $this->getServerRequest('172.16.1.3'); @@ -63,15 +53,15 @@ public function testIGetEmptyIpFromUtilsWithExcludedPrivateIpWhenUseMyPrivateIp( self::assertEmpty((new IpResolver())->resolve($serverRequest, true)); } - /** - * @param string $ip - * @return ServerRequestInterface - */ - private function getServerRequest(string $ip): ServerRequestInterface + private function getServerRequest(string $ip, string $xForwardedFor = ''): ServerRequestInterface { $server = $_SERVER; $server['HTTP_X_FORWARDED'] = $ip; + if ($xForwardedFor !== '') { + $server['HTTP_X_FORWARDED_FOR'] = $xForwardedFor; + } + $httpFactory = new Psr17Factory(); return $httpFactory->createServerRequest('GET', $httpFactory->createUri('/'), $server); } diff --git a/tests/unit/Service/DataCollectionTest.php b/tests/unit/Service/DataCollectionTest.php deleted file mode 100644 index 9db88b5..0000000 --- a/tests/unit/Service/DataCollectionTest.php +++ /dev/null @@ -1,52 +0,0 @@ - 1, 'two' => 2]; - $collection = new DataCollection($data); - - self::assertEquals(2, $collection->length()); - - foreach ($collection as $key => $value) { - self::assertEquals($data[$key], $value); - } - - $collection->reset(); - - self::assertEquals($data, $collection->toArray()); - } -} From 26098ee988996b41d4d29bea0dbce5d37637493c Mon Sep 17 00:00:00 2001 From: velkuns Date: Tue, 6 Jan 2026 17:00:51 +0100 Subject: [PATCH 2/3] - Update github workflow configs --- .github/workflows/ci.yml | 28 ++++++------------- .github/workflows/sonarcloud.yml | 46 ++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/sonarcloud.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index deef99c..10b872c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,17 +7,17 @@ on: branches: [ master ] jobs: - build: + ci: runs-on: ubuntu-latest strategy: matrix: - php-versions: [ '8.1', '8.2', '8.3' ] + php-versions: [ '8.3', '8.4', '8.5' ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: - fetch-depth: 0 # To avoid "Shallow clone detected" error in SonarCloud report + fetch-depth: 0 #~ PHP Setup - name: Setup PHP ${{ matrix.php-versions }} @@ -48,30 +48,18 @@ jobs: #~ CI part - name: Dependencies - run: make deps + run: make php/deps - name: Check Code Style - run: make phpcs + run: make php/check - name: Units Tests - run: make tests + run: make php/tests - name: Fix unit tests report path run: | sed -i 's+'$GITHUB_WORKSPACE'+/github/workspace+g' build/reports/phpunit/clover.xml sed -i 's+'$GITHUB_WORKSPACE'+/github/workspace+g' build/reports/phpunit/unit.xml - - name: PHP 8.1 Compatibility - run: make php81compatibility - - - name: PHP 8.3 Compatibility - run: make php83compatibility - - name: PHP Static Analyze - run: make analyze - - - name: SonarCloud Scan - uses: SonarSource/sonarcloud-github-action@v2.1.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: make php/analyze diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml new file mode 100644 index 0000000..9130fbb --- /dev/null +++ b/.github/workflows/sonarcloud.yml @@ -0,0 +1,46 @@ +name: SonarQube + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + sonarcloud: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # To avoid "Shallow clone detected" error in SonarCloud report + + #~ PHP Setup + - name: Setup PHP 8.3 # Minimum version + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + + #~ Composer Cache + - name: Get Composer Cache Directory + id: composer-cache + run: | + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Install dependencies + if: steps.composer-cache.outputs.cache-hit != 'true' + run: make install + + - name: Units Tests + run: make php/tests # To generate the coverage report + + - name: SonarQube Scan + uses: SonarSource/sonarqube-scan-action@v7 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From fc1fe347db0b6aca4c65cbd7aef2e6181f828cc9 Mon Sep 17 00:00:00 2001 From: velkuns Date: Tue, 6 Jan 2026 17:15:37 +0100 Subject: [PATCH 3/3] - Add dedicated KernelException --- src/Exception/KernelException.php | 14 ++++++++++++++ src/Kernel.php | 5 +++-- 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 src/Exception/KernelException.php diff --git a/src/Exception/KernelException.php b/src/Exception/KernelException.php new file mode 100644 index 0000000..06e5bcd --- /dev/null +++ b/src/Exception/KernelException.php @@ -0,0 +1,14 @@ +