diff --git a/composer.json b/composer.json index 960e2f8..90d6fcf 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ ], "type": "library", "require": { - "php": ">=7.4 <8.5" + "php": ">=7.4 <=8.4.1" }, "license": "MIT", "autoload": { @@ -31,20 +31,45 @@ ], "minimum-stability": "stable", "require-dev": { + "php-mock/php-mock-phpunit": "^2.10", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.1", "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.4", "squizlabs/php_codesniffer": "^3.11", - "php-mock/php-mock-phpunit": "^2.10", - "php-parallel-lint/php-parallel-lint": "^1.4", - "phpstan/phpstan": "^2.0", - "vimeo/psalm": "^0.3.14" + "vimeo/psalm": "^4.9" + }, + "scripts": { + "analyze": [ + "@phpstan", + "@psalm" + ], + "build:clean": "git clean -fX build/", + "lint": "parallel-lint src tests", + "lint:paths": "parallel-lint", + "phpcs": "phpcs --standard=PSR12 --exclude=Generic.Files.LineLength", + "phpstan": [ + "phpstan analyse --no-progress --memory-limit=1G", + "phpstan analyse -c phpstan-tests.neon --no-progress --memory-limit=1G" + ], + "phpunit": "phpunit --verbose --colors=always", + "phpunit-coverage": "phpunit --verbose --colors=always --coverage-html build/coverage", + "psalm": "psalm --show-info=false --config=psalm.xml", + "test": [ + "@lint", + "@phpstan", + "@psalm", + "@phpunit" + ] }, "archive": { - "exclude": ["examples"] + "exclude": ["example"] }, - "scripts": { - "lint": "parallel-lint", - "phpstan": "phpstan analyse --memory-limit=512M", - "psalm": "psalm", - "phpcs": "phpcs --standard=PSR12" + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } } } diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..aac5769 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,476 @@ + + + + + + + + ./src + ./tests + + A common coding standard for Ramsey's PHP libraries. + + + + + + + + + + + + + + + + + error + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 + + + + + + + + + + + + + + diff --git a/phpstan-tests.neon b/phpstan-tests.neon new file mode 100644 index 0000000..d55696e --- /dev/null +++ b/phpstan-tests.neon @@ -0,0 +1,6 @@ +parameters: + tmpDir: ./build/cache/phpstan + level: max + paths: + - ./tests + reportUnmatchedIgnoredErrors: false diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..f478a1e --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,5 @@ +parameters: + tmpDir: ./build/cache/phpstan + level: max + paths: + - ./src diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..8dc065a --- /dev/null +++ b/psalm.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/src/Exception/CallbackException.php b/src/Exception/CallbackException.php index d7f7b2e..b5c3d65 100644 --- a/src/Exception/CallbackException.php +++ b/src/Exception/CallbackException.php @@ -13,7 +13,7 @@ public function __construct( array $errorDetails, $message = "Callback Exception", $code = 0, - Throwable $previous = null + ?Throwable $previous = null ) { $this->errorDetails = $errorDetails; parent::__construct($message, $code, $previous); diff --git a/src/Exception/SameSiteCallbackException.php b/src/Exception/SameSiteCallbackException.php index d36bd16..2930bf6 100644 --- a/src/Exception/SameSiteCallbackException.php +++ b/src/Exception/SameSiteCallbackException.php @@ -12,7 +12,7 @@ class SameSiteCallbackException extends Exception public function __construct( $message = "Same Site Callback Exception", $code = 0, - Throwable $previous = null + ?Throwable $previous = null ) { parent::__construct($message, $code, $previous); } diff --git a/src/Handler/Callback.php b/src/Handler/Callback.php index 9ee6126..da2b85a 100644 --- a/src/Handler/Callback.php +++ b/src/Handler/Callback.php @@ -16,6 +16,7 @@ use HelloCoop\Lib\TokenParser; use Exception; use HelloCoop\Utils\CurlWrapper; +use Throwable; class Callback { @@ -260,13 +261,13 @@ public function handleCallback(): ?string * * @param array $error Error details including 'target_uri', 'error', and 'error_description'. * @param string $errorMessage A message describing the error. - * @param \Throwable|null $previous Previous exception for chaining (optional). + * @param Throwable|null $previous Previous exception for chaining (optional). * * @return string The error page URL. * * @throws CallbackException If no error URI is provided. */ - private function sendErrorPage(array $error, string $errorMessage, \Throwable $previous = null): string + private function sendErrorPage(array $error, string $errorMessage, ?Throwable $previous = null): string { $error_uri = $error['target_uri'] ?? $this->config->getRoutes()['error'] ?? null; if ($error_uri) { diff --git a/src/Handler/Logout.php b/src/Handler/Logout.php index 8dfe956..e78f7ca 100644 --- a/src/Handler/Logout.php +++ b/src/Handler/Logout.php @@ -7,12 +7,23 @@ use HelloCoop\Config\ConfigInterface; use HelloCoop\Lib\Auth as AuthLib; +/** + * Handles user logout functionality, including generating the logout URL and clearing authentication cookies. + */ class Logout { private HelloResponseInterface $helloResponse; private HelloRequestInterface $helloRequest; private ConfigInterface $config; private ?AuthLib $authLib = null; + + /** + * Constructor for the Logout class. + * + * @param HelloRequestInterface $helloRequest The request object for fetching data. + * @param HelloResponseInterface $helloResponse The response object for sending data. + * @param ConfigInterface $config The configuration object. + */ public function __construct( HelloRequestInterface $helloRequest, HelloResponseInterface $helloResponse, @@ -23,6 +34,11 @@ public function __construct( $this->config = $config; } + /** + * Retrieves the AuthLib instance. + * + * @return AuthLib The authentication library instance. + */ private function getAuthLib(): AuthLib { return $this->authLib ??= new AuthLib( @@ -32,13 +48,18 @@ private function getAuthLib(): AuthLib ); } + /** + * Generates the URL to redirect to after logout. + * + * @return string The logout redirect URL. + */ public function generateLogoutUrl(): string { $targetUri = $this->helloRequest->fetch('target_uri'); $this->getAuthLib()->clearAuthCookie(); - if ($this->config->getLoginSync()) { + if ($this->config->getLogoutSync()) { // Call the logoutSync callback - call_user_func($this->config->getLoginSync()); + call_user_func($this->config->getLogoutSync()); } return $targetUri ?? $this->config->getRoutes()['loggedOut']; } diff --git a/src/HelloClient.php b/src/HelloClient.php index 35eb821..554d562 100644 --- a/src/HelloClient.php +++ b/src/HelloClient.php @@ -31,9 +31,9 @@ class HelloClient public function __construct( ConfigInterface $config, - HelloRequestInterface $helloRequest = null, - HelloResponseInterface $helloResponse = null, - PageRendererInterface $pageRenderer = null + ?HelloRequestInterface $helloRequest = null, + ?HelloResponseInterface $helloResponse = null, + ?PageRendererInterface $pageRenderer = null ) { $this->config = $config; $this->helloRequest = $helloRequest ??= new HelloRequest(); diff --git a/src/Lib/PKCE.php b/src/Lib/PKCE.php index 79d5a3a..f73ce8c 100644 --- a/src/Lib/PKCE.php +++ b/src/Lib/PKCE.php @@ -4,7 +4,7 @@ class PKCE { - const VERIFIER_LENGTH = 43; + public const VERIFIER_LENGTH = 43; /** Generate cryptographically strong random string * @param int $size The desired length of the string diff --git a/src/Type/AuthCookie.php b/src/Type/AuthCookie.php index d24d51f..3dd2356 100644 --- a/src/Type/AuthCookie.php +++ b/src/Type/AuthCookie.php @@ -5,7 +5,8 @@ use InvalidArgumentException; // Authentication cookie class, extending Claims -class AuthCookie extends Claims { +class AuthCookie extends Claims +{ /** @var int */ public $iat; @@ -15,7 +16,8 @@ class AuthCookie extends Claims { */ public $extraProperties = []; - public function __construct(string $sub, int $iat) { + public function __construct(string $sub, int $iat) + { parent::__construct($sub); $this->iat = $iat; } @@ -23,21 +25,24 @@ public function __construct(string $sub, int $iat) { /** * Add an extra property. */ - public function setExtraProperty(string $key, $value): void { + public function setExtraProperty(string $key, $value): void + { $this->extraProperties[$key] = $value; } /** * Get an extra property. */ - public function getExtraProperty(string $key) { + public function getExtraProperty(string $key) + { return $this->extraProperties[$key] ?? null; } /** * Create an instance from an array of key-value pairs. */ - public static function fromArray(array $data): self { + public static function fromArray(array $data): self + { if (!isset($data['sub'], $data['iat'])) { throw new InvalidArgumentException('Missing required keys "sub" or "iat".'); } @@ -52,11 +57,12 @@ public static function fromArray(array $data): self { return $instance; } - + /** * Convert the instance to an array of key-value pairs. */ - public function toArray(): array { + public function toArray(): array + { return array_merge(['sub' => $this->sub, 'iat' => $this->iat], $this->extraProperties); } -} \ No newline at end of file +} diff --git a/src/Type/AuthUpdates.php b/src/Type/AuthUpdates.php index 5178dab..cf4586a 100644 --- a/src/Type/AuthUpdates.php +++ b/src/Type/AuthUpdates.php @@ -1,49 +1,61 @@ additionalProperties = $updates; } // Magic methods for dynamic properties - public function __set(string $name, $value): void { + public function __set(string $name, $value): void + { $this->additionalProperties[$name] = $value; } - public function __get(string $name) { + public function __get(string $name) + { return $this->additionalProperties[$name] ?? null; } - public function __isset(string $name): bool { + public function __isset(string $name): bool + { return isset($this->additionalProperties[$name]); } - public function __unset(string $name): void { + public function __unset(string $name): void + { unset($this->additionalProperties[$name]); } // ArrayAccess implementation - public function offsetExists($offset): bool { + public function offsetExists($offset): bool + { return isset($this->additionalProperties[$offset]); } - public function offsetGet($offset) { + public function offsetGet($offset): ?string + { return $this->additionalProperties[$offset] ?? null; } - public function offsetSet($offset, $value): void { + public function offsetSet($offset, $value): void + { $this->additionalProperties[$offset] = $value; } - public function offsetUnset($offset): void { + public function offsetUnset($offset): void + { unset($this->additionalProperties[$offset]); } - public function toArray(): array { + public function toArray(): array + { return array_merge(get_object_vars($this), $this->additionalProperties); } -} \ No newline at end of file +} diff --git a/src/Type/Claims.php b/src/Type/Claims.php index b544130..5ca36ea 100644 --- a/src/Type/Claims.php +++ b/src/Type/Claims.php @@ -6,13 +6,17 @@ use HelloCoop\Type\Common\OptionalAccountClaimsTrait; use HelloCoop\Type\Common\OptionalOrgClaimTrait; -class Claims { - use OptionalStringClaimsTrait, OptionalAccountClaimsTrait, OptionalOrgClaimTrait; +class Claims +{ + use OptionalStringClaimsTrait; + use OptionalAccountClaimsTrait; + use OptionalOrgClaimTrait; /** @var string */ public $sub; - public function __construct(string $sub) { + public function __construct(string $sub) + { $this->sub = $sub; } -} \ No newline at end of file +} diff --git a/src/Type/Common/OptionalAccountClaimsTrait.php b/src/Type/Common/OptionalAccountClaimsTrait.php index c05f72d..27c866b 100644 --- a/src/Type/Common/OptionalAccountClaimsTrait.php +++ b/src/Type/Common/OptionalAccountClaimsTrait.php @@ -2,10 +2,11 @@ namespace HelloCoop\Type\Common; -trait OptionalAccountClaimsTrait { +trait OptionalAccountClaimsTrait +{ /** * @var array * An associative array of account claims (e.g., ['github' => ['id' => '123', 'username' => 'user']]). */ public $claims = []; -} \ No newline at end of file +} diff --git a/src/Type/Common/OptionalOrgClaimTrait.php b/src/Type/Common/OptionalOrgClaimTrait.php index 4e97232..6bef94c 100644 --- a/src/Type/Common/OptionalOrgClaimTrait.php +++ b/src/Type/Common/OptionalOrgClaimTrait.php @@ -2,10 +2,11 @@ namespace HelloCoop\Type\Common; -trait OptionalOrgClaimTrait { +trait OptionalOrgClaimTrait +{ /** * @var array{id: string, domain: string}|null * Optional organization claim. */ public $org; -} \ No newline at end of file +} diff --git a/src/Type/Common/OptionalStringClaimsTrait.php b/src/Type/Common/OptionalStringClaimsTrait.php index 9798c9c..d86c8f9 100644 --- a/src/Type/Common/OptionalStringClaimsTrait.php +++ b/src/Type/Common/OptionalStringClaimsTrait.php @@ -1,7 +1,9 @@ codeVerifier = $codeVerifier; $this->nonce = $nonce; $this->redirectUri = $redirectUri; $this->targetUri = $targetUri; } - public static function fromArray(array $data): self { + public static function fromArray(array $data): self + { if (!isset($data['code_verifier'])) { throw new InvalidArgumentException('Missing code_verifier'); } @@ -42,7 +45,8 @@ public static function fromArray(array $data): self { ); } - public function toArray(): array { + public function toArray(): array + { return [ 'code_verifier' => $this->codeVerifier, 'nonce' => $this->nonce, @@ -51,4 +55,3 @@ public function toArray(): array { ]; } } - diff --git a/tests/Handler/LogoutTest.php b/tests/Handler/LogoutTest.php index d2b343d..c59236a 100644 --- a/tests/Handler/LogoutTest.php +++ b/tests/Handler/LogoutTest.php @@ -77,7 +77,7 @@ public function testGenerateLogoutUrlWithLoginSync(): void ->method('__invoke'); $this->configMock - ->method('getLoginSync') + ->method('getLogoutSync') ->willReturn($syncCallback); $this->configMock diff --git a/tests/Type/AuthCookieTest.php b/tests/Type/AuthCookieTest.php index 7846e97..1993502 100644 --- a/tests/Type/AuthCookieTest.php +++ b/tests/Type/AuthCookieTest.php @@ -56,4 +56,4 @@ public function testFromArrayThrowsExceptionForMissingKeys() $data = ['sub' => 'user123']; // Missing 'iat' AuthCookie::fromArray($data); } -} \ No newline at end of file +} diff --git a/tests/Type/AuthTest.php b/tests/Type/AuthTest.php index 2db3e30..a8ceccb 100644 --- a/tests/Type/AuthTest.php +++ b/tests/Type/AuthTest.php @@ -12,7 +12,7 @@ public function testConstructorInitializesProperties() { $authCookie = new AuthCookie('user123', time()); $authCookie->setExtraProperty('role', 'admin'); - + $auth = new Auth(true, $authCookie, 'token123'); $this->assertTrue($auth->isLoggedIn); diff --git a/tests/Type/OIDCTest.php b/tests/Type/OIDCTest.php index ca95b96..26c1b17 100644 --- a/tests/Type/OIDCTest.php +++ b/tests/Type/OIDCTest.php @@ -5,8 +5,10 @@ use HelloCoop\Type\OIDC; use PHPUnit\Framework\TestCase; -class OIDCTest extends TestCase { - public function testFromArrayValidData(): void { +class OIDCTest extends TestCase +{ + public function testFromArrayValidData(): void + { $data = [ 'code_verifier' => 'test_verifier', 'nonce' => 'test_nonce', @@ -23,7 +25,8 @@ public function testFromArrayValidData(): void { $this->assertEquals('/home', $oidc->targetUri); } - public function testFromArrayMissingKeys(): void { + public function testFromArrayMissingKeys(): void + { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Missing code_verifier'); @@ -36,7 +39,8 @@ public function testFromArrayMissingKeys(): void { OIDC::fromArray($data); } - public function testToArray(): void { + public function testToArray(): void + { $oidc = new OIDC('test_verifier', 'test_nonce', 'https://example.com/callback', '/home'); $expected = [ diff --git a/tests/Utils/QueryParamFetcherTest.php b/tests/Utils/QueryParamFetcherTest.php index a4249cb..c47e187 100644 --- a/tests/Utils/QueryParamFetcherTest.php +++ b/tests/Utils/QueryParamFetcherTest.php @@ -7,6 +7,7 @@ class QueryParamFetcherTest extends TestCase { + protected array $originalGet; protected function setUp(): void { // Backup the original $_GET superglobal