diff --git a/.gitattributes b/.gitattributes index fe5c282..f82fa3a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,4 +6,5 @@ /CONTRIBUTING.md export-ignore /phpcs.xml export-ignore /kahlan-config.php export-ignore -/rector.php export-ignore \ No newline at end of file +/rector.php export-ignore +/phpstan-baseline.neon export-ignore diff --git a/.github/workflows/ci_build.yml b/.github/workflows/ci_build.yml index a67c08b..1c836fc 100644 --- a/.github/workflows/ci_build.yml +++ b/.github/workflows/ci_build.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - php-versions: ['7.4', '8.0', '8.1'] + php-versions: ['8.2', '8.3', '8.4'] steps: - name: Setup PHP Action uses: shivammathur/setup-php@v2 @@ -30,11 +30,12 @@ jobs: run: "composer cs-check" - name: "Code analyze" run: | - bin/phpstan analyse src/ --level=8 -c phpstan.neon + bin/phpstan analyse src/ --level=max -c phpstan.neon bin/rector process --dry-run - name: "Run test suite" run: "mkdir -p build/logs && bin/kahlan --coverage=4 --reporter=verbose --clover=build/logs/clover.xml" - name: Upload coverage to Codecov + if: github.event.pull_request.head.repo.full_name == 'samsonasik/ForceHttpsModule' uses: codecov/codecov-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/composer.json b/composer.json index f152210..2d2dd59 100644 --- a/composer.json +++ b/composer.json @@ -24,21 +24,21 @@ } ], "require": { - "php": "^7.4|^8.0", - "webmozart/assert": "^1.9" + "php": "^8.2", + "webmozart/assert": "^1.11" }, "conflict": { "mezzio/mezzio": "<3.0", "laminas/laminas-mvc": "<3.0" }, "require-dev": { - "kahlan/kahlan": "^5.2", - "laminas/laminas-coding-standard": "^2.0", - "laminas/laminas-mvc": "^3.0", - "mezzio/mezzio": "^3.0", - "php-coveralls/php-coveralls": "^2.1", - "phpstan/phpstan": "^1.1", - "phpstan/phpstan-webmozart-assert": "^1.0", + "kahlan/kahlan": "^6.0", + "laminas/laminas-coding-standard": "^2.5", + "laminas/laminas-mvc": "^3.8", + "mezzio/mezzio": "^3.20.1", + "php-coveralls/php-coveralls": "^2.7", + "phpstan/phpstan": "^2.0.4", + "phpstan/phpstan-webmozart-assert": "^2.0", "rector/rector": "dev-main" }, "config": { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..8f1c10b --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,103 @@ +parameters: + ignoreErrors: + - + message: '#^Cannot access offset ''enable'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 2 + path: src/Listener/ForceHttps.php + + - + message: '#^Cannot access offset ''value'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 2 + path: src/Listener/ForceHttps.php + + - + message: '#^Method ForceHttpsModule\\Listener\\ForceHttps\:\:isGoingToBeForcedToHttps\(\) should return bool but returns mixed\.$#' + identifier: return.type + count: 1 + path: src/Listener/ForceHttps.php + + - + message: '#^Parameter \#2 \$haystack of function in_array expects array, mixed given\.$#' + identifier: argument.type + count: 2 + path: src/Listener/ForceHttps.php + + - + message: '#^Parameter \#2 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Listener/ForceHttps.php + + - + message: '#^Property ForceHttpsModule\\Listener\\ForceHttps\:\:\$needsWwwPrefix \(bool\) does not accept mixed\.$#' + identifier: assign.propertyType + count: 1 + path: src/Listener/ForceHttps.php + + - + message: '#^Cannot access offset ''force\-https\-module'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: src/Listener/ForceHttpsFactory.php + + - + message: '#^Parameter \#1 \$config of class ForceHttpsModule\\Listener\\ForceHttps constructor expects array\, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Listener/ForceHttpsFactory.php + + - + message: '#^Cannot access offset ''enable'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 2 + path: src/Middleware/ForceHttps.php + + - + message: '#^Cannot access offset ''value'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 2 + path: src/Middleware/ForceHttps.php + + - + message: '#^Method ForceHttpsModule\\Middleware\\ForceHttps\:\:isGoingToBeForcedToHttps\(\) should return bool but returns mixed\.$#' + identifier: return.type + count: 1 + path: src/Middleware/ForceHttps.php + + - + message: '#^Parameter \#2 \$haystack of function in_array expects array, mixed given\.$#' + identifier: argument.type + count: 2 + path: src/Middleware/ForceHttps.php + + - + message: '#^Parameter \#2 \$value of method Psr\\Http\\Message\\MessageInterface\:\:withHeader\(\) expects array\\|string, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Middleware/ForceHttps.php + + - + message: '#^Property ForceHttpsModule\\Middleware\\ForceHttps\:\:\$needsWwwPrefix \(bool\) does not accept mixed\.$#' + identifier: assign.propertyType + count: 1 + path: src/Middleware/ForceHttps.php + + - + message: '#^Cannot access offset ''force\-https\-module'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: src/Middleware/ForceHttpsFactory.php + + - + message: '#^Parameter \#1 \$config of class ForceHttpsModule\\Middleware\\ForceHttps constructor expects array\, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Middleware/ForceHttpsFactory.php + + - + message: '#^Method ForceHttpsModule\\Module\:\:getConfig\(\) should return array\ but returns mixed\.$#' + identifier: return.type + count: 1 + path: src/Module.php diff --git a/phpstan.neon b/phpstan.neon index 8f6ad3d..5a23f3d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,3 @@ includes: - vendor/phpstan/phpstan-webmozart-assert/extension.neon - -parameters: - checkMissingIterableValueType: false - + - phpstan-baseline.neon diff --git a/rector.php b/rector.php index 6fe2fbe..a020c9b 100644 --- a/rector.php +++ b/rector.php @@ -2,37 +2,20 @@ declare(strict_types=1); -use Rector\CodeQuality\Rector\Array_\CallableThisArrayToAnonymousFunctionRector; -use Rector\CodingStyle\Rector\ArrowFunction\StaticArrowFunctionRector; -use Rector\CodingStyle\Rector\Closure\StaticClosureRector; use Rector\Config\RectorConfig; -use Rector\Set\ValueObject\LevelSetList; -use Rector\Set\ValueObject\SetList; +use Rector\Php81\Rector\Array_\FirstClassCallableRector; -return static function (RectorConfig $rectorConfig): void { - $rectorConfig->sets([ - SetList::DEAD_CODE, - LevelSetList::UP_TO_PHP_74, - SetList::CODE_QUALITY, - SetList::NAMING, - SetList::TYPE_DECLARATION, - SetList::CODING_STYLE, - ]); - - $rectorConfig->paths([ +return RectorConfig::configure() + ->withPhpSets(php82: true) + ->withPreparedSets(deadCode: true, codeQuality: true, naming: true, typeDeclarations: true, codingStyle: true) + ->withImportNames(removeUnusedImports: true) + ->withPaths([ __DIR__ . '/config', __DIR__ . '/src', __DIR__ . '/spec', __DIR__ . '/rector.php' + ]) + ->withRootFiles() + ->withSkip([ + FirstClassCallableRector::class, ]); - $rectorConfig->importNames(); - $rectorConfig->skip([ - CallableThisArrayToAnonymousFunctionRector::class, - StaticArrowFunctionRector::class => [ - __DIR__ . '/spec', - ], - StaticClosureRector::class => [ - __DIR__ . '/spec', - ], - ]); -}; diff --git a/spec/Listener/ForceHttpsSpec.php b/spec/Listener/ForceHttpsSpec.php index 079bba2..05b9fbb 100644 --- a/spec/Listener/ForceHttpsSpec.php +++ b/spec/Listener/ForceHttpsSpec.php @@ -6,7 +6,6 @@ use Kahlan\Plugin\Double; use Kahlan\Plugin\Quit; use Kahlan\QuitException; -use Laminas\Console\Console; use Laminas\EventManager\EventManagerInterface; use Laminas\Http\PhpEnvironment\Request; use Laminas\Http\PhpEnvironment\Response; diff --git a/spec/Middleware/ForceHttpsSpec.php b/spec/Middleware/ForceHttpsSpec.php index e8a5cbc..750e5a3 100644 --- a/spec/Middleware/ForceHttpsSpec.php +++ b/spec/Middleware/ForceHttpsSpec.php @@ -39,9 +39,12 @@ it('not redirect on router not match', function (): void { - $match = RouteResult::fromRouteFailure(null); - allow($this->router)->toReceive('match')->andReturn($match); - allow($this->request)->toReceive('getUri', 'getScheme')->andReturn('http'); + $routeResult = RouteResult::fromRouteFailure(null); + + allow($this->router)->toReceive('match')->andReturn($routeResult); + + allow($this->request)->toReceive('getUri')->andReturn($this->uri); + allow($this->uri)->toReceive('getScheme')->andReturn('http'); $listener = new ForceHttps(['enable' => true], $this->router); @@ -56,9 +59,13 @@ it('not redirect on router not match and config allow_404 is false', function (): void { - $match = RouteResult::fromRouteFailure(null); - allow($this->router)->toReceive('match')->andReturn($match); - allow($this->request)->toReceive('getUri', 'getScheme')->andReturn('http'); + $routeResult = RouteResult::fromRouteFailure(null); + + allow($this->router)->toReceive('match')->andReturn($routeResult); + + allow($this->request)->toReceive('getUri')->andReturn($this->uri); + allow($this->uri)->toReceive('__toString')->andReturn('http://example.com/404'); + allow($this->uri)->toReceive('getScheme')->andReturn('http'); $listener = new ForceHttps( [ @@ -79,11 +86,13 @@ it('not redirect on https and match but no strict_transport_security config', function (): void { - $match = RouteResult::fromRoute(new Route('/about', Double::instance(['implements' => MiddlewareInterface::class]))); + $routeResult = RouteResult::fromRoute(new Route('/about', Double::instance(['implements' => MiddlewareInterface::class]))); - allow($this->router)->toReceive('match')->andReturn($match); + allow($this->router)->toReceive('match')->andReturn($routeResult); - allow($this->request)->toReceive('getUri', 'getScheme')->andReturn('https'); + allow($this->request)->toReceive('getUri')->andReturn($this->uri); + allow($this->uri)->toReceive('__toString')->andReturn('https://example.com/about'); + allow($this->uri)->toReceive('getScheme')->andReturn('https'); $listener = new ForceHttps(['enable' => true, 'force_all_routes' => true], $this->router); @@ -98,10 +107,12 @@ it('not redirect on http and match, with force_all_routes is false and matched route name not in force_specific_routes config', function (): void { - $match = RouteResult::fromRoute(new Route('/about', Double::instance(['implements' => MiddlewareInterface::class]))); - allow($this->router)->toReceive('match')->andReturn($match); + $routeResult = RouteResult::fromRoute(new Route('/about', Double::instance(['implements' => MiddlewareInterface::class]))); + allow($this->router)->toReceive('match')->andReturn($routeResult); - allow($this->request)->toReceive('getUri', 'getScheme')->andReturn('http'); + allow($this->request)->toReceive('getUri')->andReturn($this->uri); + allow($this->uri)->toReceive('__toString')->andReturn('http://example.com/about'); + allow($this->uri)->toReceive('getScheme')->andReturn('http'); $listener = new ForceHttps( [ @@ -127,11 +138,13 @@ it('not redirect on https and match, with strict_transport_security config, but disabled', function (): void { - $match = RouteResult::fromRoute(new Route('/about', Double::instance(['implements' => MiddlewareInterface::class]))); + $routeResult = RouteResult::fromRoute(new Route('/about', Double::instance(['implements' => MiddlewareInterface::class]))); - allow($this->router)->toReceive('match')->andReturn($match); + allow($this->router)->toReceive('match')->andReturn($routeResult); - allow($this->request)->toReceive('getUri', 'getScheme')->andReturn('https'); + allow($this->request)->toReceive('getUri')->andReturn($this->uri); + allow($this->uri)->toReceive('__toString')->andReturn('https://example.com/about'); + allow($this->uri)->toReceive('getScheme')->andReturn('https'); $listener = new ForceHttps( [ @@ -157,10 +170,12 @@ it('not redirect on https and match, with strict_transport_security config, and enabled', function (): void { - $match = RouteResult::fromRoute(new Route('/about', Double::instance(['implements' => MiddlewareInterface::class]))); + $routeResult = RouteResult::fromRoute(new Route('/about', Double::instance(['implements' => MiddlewareInterface::class]))); - allow($this->router)->toReceive('match')->andReturn($match); - allow($this->request)->toReceive('getUri', 'getScheme')->andReturn('https'); + allow($this->router)->toReceive('match')->andReturn($routeResult); + allow($this->request)->toReceive('getUri')->andReturn($this->uri); + allow($this->uri)->toReceive('__toString')->andReturn('https://example.com/about'); + allow($this->uri)->toReceive('getScheme')->andReturn('https'); $listener = new ForceHttps( [ @@ -186,11 +201,14 @@ it('return Response with 308 status on http and match', function (): void { - $match = RouteResult::fromRoute(new Route('/about', Double::instance(['implements' => MiddlewareInterface::class]))); + $routeResult = RouteResult::fromRoute(new Route('/about', Double::instance(['implements' => MiddlewareInterface::class]))); + + allow($this->router)->toReceive('match')->andReturn($routeResult); - allow($this->router)->toReceive('match')->andReturn($match); - allow($this->request)->toReceive('getUri', 'getScheme')->andReturn('http'); - allow($this->request)->toReceive('getUri', 'withScheme', '__toString')->andReturn('https://example.com/about'); + allow($this->request)->toReceive('getUri')->andReturn($this->uri); + allow($this->uri)->toReceive('getScheme')->andReturn('http'); + allow($this->uri)->toReceive('withScheme')->andReturn($this->uri); + allow($this->uri)->toReceive('__toString')->andReturn('https://example.com/about'); $handler = Double::instance(['implements' => RequestHandlerInterface::class]); allow($handler)->toReceive('handle')->with($this->request)->andReturn($this->response); @@ -217,11 +235,14 @@ it('return Response with 308 status on http and not match, but allow_404 is true', function (): void { - $match = RouteResult::fromRouteFailure(null); + $routeResult = RouteResult::fromRouteFailure(null); - allow($this->router)->toReceive('match')->andReturn($match); - allow($this->request)->toReceive('getUri', 'getScheme')->andReturn('http'); - allow($this->request)->toReceive('getUri', 'withScheme', '__toString')->andReturn('https://example.com/404'); + allow($this->router)->toReceive('match')->andReturn($routeResult); + + allow($this->request)->toReceive('getUri')->andReturn($this->uri); + allow($this->uri)->toReceive('getScheme')->andReturn('http'); + allow($this->uri)->toReceive('withScheme')->andReturn($this->uri); + allow($this->uri)->toReceive('__toString')->andReturn('https://example.com/404'); $handler = Double::instance(['implements' => RequestHandlerInterface::class]); allow($handler)->toReceive('handle')->with($this->request)->andReturn($this->response); @@ -244,11 +265,14 @@ it('return Response with 308 status with include www prefix on http and match with configurable "add_www_prefix"', function (): void { - $match = RouteResult::fromRoute(new Route('/about', Double::instance(['implements' => MiddlewareInterface::class]))); + $routeResult = RouteResult::fromRoute(new Route('/about', Double::instance(['implements' => MiddlewareInterface::class]))); - allow($this->router)->toReceive('match')->andReturn($match); - allow($this->request)->toReceive('getUri', 'getScheme')->andReturn('http'); - allow($this->request)->toReceive('getUri', 'withScheme', '__toString')->andReturn('https://example.com/about'); + allow($this->router)->toReceive('match')->andReturn($routeResult); + allow($this->request)->toReceive('getUri')->andReturn($this->uri); + allow($this->uri)->toReceive('__toString')->andReturn('https://example.com/about'); + allow($this->uri)->toReceive('getScheme')->andReturn('http'); + allow($this->uri)->toReceive('withScheme')->andReturn($this->uri); + allow($this->uri)->toReceive('__toString')->andReturn('https://www.example.com/about'); $handler = Double::instance(['implements' => RequestHandlerInterface::class]); allow($handler)->toReceive('handle')->with($this->request)->andReturn($this->response); @@ -276,16 +300,14 @@ it('return Response with 308 status with remove www prefix on http and match with configurable "remove_www_prefix"', function (): void { - $match = RouteResult::fromRoute(new Route('/about', Double::instance(['implements' => MiddlewareInterface::class]))); + $routeResult = RouteResult::fromRoute(new Route('/about', Double::instance(['implements' => MiddlewareInterface::class]))); - allow($this->request)->toReceive('getUri', '__toString')->andReturn('http://www.example.com/about'); - allow($this->router)->toReceive('match')->andReturn($match); - allow($this->request)->toReceive('getUri', 'getScheme')->andReturn('http'); - allow($this->request)->toReceive('getUri', 'withScheme', '__toString')->andReturn('https://www.example.com/about'); - - allow($this->response)->toReceive('withStatus')->andReturn($this->response); - $handler = Double::instance(['implements' => RequestHandlerInterface::class]); - allow($handler)->toReceive('handle')->with($this->request)->andReturn($this->response); + allow($this->router)->toReceive('match')->andReturn($routeResult); + allow($this->uri)->toReceive('__toString')->andReturn('https://www.example.com/about'); + allow($this->request)->toReceive('getUri')->andReturn($this->uri); + allow($this->uri)->toReceive('getScheme')->andReturn('http'); + allow($this->uri)->toReceive('withScheme')->andReturn($this->uri); + allow($this->uri)->toReceive('__toString')->andReturn('https://example.com/about'); $handler = Double::instance(['implements' => RequestHandlerInterface::class]); allow($handler)->toReceive('handle')->with($this->request)->andReturn($this->response); @@ -305,7 +327,6 @@ ], $this->router ); - $listener->process($this->request, $handler); expect($this->response)->toReceive('withStatus')->with(308); diff --git a/src/HttpsTrait.php b/src/HttpsTrait.php index dcc9c84..c71367a 100644 --- a/src/HttpsTrait.php +++ b/src/HttpsTrait.php @@ -6,7 +6,6 @@ use Laminas\Router\RouteMatch; use Mezzio\Router\RouteResult; -use Webmozart\Assert\Assert; use function in_array; use function strpos; @@ -14,11 +13,9 @@ trait HttpsTrait { - /** @var bool */ - private $needsWwwPrefix = false; + private bool $needsWwwPrefix = false; - /** @var bool */ - private $alreadyHasWwwPrefix = false; + private bool $alreadyHasWwwPrefix = false; private function isSchemeHttps(string $uriScheme): bool { @@ -27,16 +24,13 @@ private function isSchemeHttps(string $uriScheme): bool /** * Check Config if is going to be forced to https. - * - * @param RouteMatch|RouteResult|null $match */ - private function isGoingToBeForcedToHttps($match = null): bool + private function isGoingToBeForcedToHttps(RouteMatch|RouteResult|null $match = null): bool { if ($match === null || ($match instanceof RouteResult && $match->isFailure())) { return $this->config['allow_404'] ?? false; } - Assert::notNull($match); $matchedRouteName = $match->getMatchedRouteName(); if ($this->config['force_all_routes']) { @@ -49,11 +43,11 @@ private function isGoingToBeForcedToHttps($match = null): bool /** * Check if Setup Strict-Transport-Security need to be skipped. - * - * @param RouteMatch|RouteResult|null $match */ - private function isSkippedHttpStrictTransportSecurity(string $uriScheme, $match = null): bool - { + private function isSkippedHttpStrictTransportSecurity( + string $uriScheme, + RouteMatch|RouteResult|null $match = null + ): bool { return ! $this->isSchemeHttps($uriScheme) || ! $this->isGoingToBeForcedToHttps($match) || ! isset( diff --git a/src/Listener/ForceHttps.php b/src/Listener/ForceHttps.php index c1c8bd2..76878a5 100644 --- a/src/Listener/ForceHttps.php +++ b/src/Listener/ForceHttps.php @@ -21,14 +21,11 @@ class ForceHttps extends AbstractListenerAggregate { use HttpsTrait; - private array $config; - /** * @param mixed[] $config */ - public function __construct(array $config) + public function __construct(private readonly array $config) { - $this->config = $config; } /** diff --git a/src/Middleware/ForceHttps.php b/src/Middleware/ForceHttps.php index 23cffe2..dc32bb6 100644 --- a/src/Middleware/ForceHttps.php +++ b/src/Middleware/ForceHttps.php @@ -16,17 +16,12 @@ class ForceHttps implements MiddlewareInterface { use HttpsTrait; - private array $config; - - private RouterInterface $router; - /** * @param mixed[] $config */ - public function __construct(array $config, RouterInterface $router) + public function __construct(private array $config, private RouterInterface $router) { $this->config = $config; - $this->router = $router; } private function setHttpStrictTransportSecurity( diff --git a/src/Middleware/ForceHttpsFactory.php b/src/Middleware/ForceHttpsFactory.php index 1784dad..82cd675 100644 --- a/src/Middleware/ForceHttpsFactory.php +++ b/src/Middleware/ForceHttpsFactory.php @@ -11,7 +11,8 @@ class ForceHttpsFactory { public function __invoke(ContainerInterface $container): ForceHttps { - $config = $container->get('config'); + $config = $container->get('config'); + /** @var RouterInterface $router */ $router = $container->get(RouterInterface::class); $forceHttpsConfig = $config['force-https-module'] ?? ['enable' => false];