diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index c6317a2..8485ff7 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -9,6 +9,14 @@ on: - '**phpunit.xml.dist' - '**psalm.xml' - '**composer.json' + pull_request: + paths: + - '**workflows/qa.yml' + - '**.php' + - '**phpcs.xml.dist' + - '**phpunit.xml.dist' + - '**psalm.xml' + - '**composer.json' workflow_dispatch: inputs: jobs: @@ -20,7 +28,8 @@ on: - 'Run all' - 'Run PHPCS only' - 'Run Psalm only' - - 'Run static analysis (PHPCS + Psalm)' + - 'Run lint only' + - 'Run static analysis (PHPCS + Psalm + lint)' - 'Run unit tests only' concurrency: @@ -28,19 +37,37 @@ concurrency: cancel-in-progress: true jobs: + lint-php: + uses: inpsyde/reusable-workflows/.github/workflows/lint-php.yml@main + if: ${{ (github.event_name != 'workflow_dispatch') || ((github.event.inputs.jobs != 'Run PHPCS only') && (github.event.inputs.jobs != 'Run Psalm only') && (github.event.inputs.jobs != 'Run unit tests only')) }} + strategy: + matrix: + php: [ '7.4', '8.0', '8.1', '8.2', '8.3' ] + with: + PHP_VERSION: ${{ matrix.php }} + coding-standards-analysis-php: - if: ${{ (github.event_name != 'workflow_dispatch') || ((github.event.inputs.jobs != 'Run unit tests only') && (github.event.inputs.jobs != 'Run Psalm only')) }} + if: ${{ (github.event_name != 'workflow_dispatch') || ((github.event.inputs.jobs != 'Run lint only') && (github.event.inputs.jobs != 'Run Psalm only') && (github.event.inputs.jobs != 'Run unit tests only')) }} uses: inpsyde/reusable-workflows/.github/workflows/coding-standards-php.yml@main + with: + PHP_VERSION: '8.0' static-code-analysis-php: - if: ${{ (github.event_name != 'workflow_dispatch') || ((github.event.inputs.jobs != 'Run unit tests only') && (github.event.inputs.jobs != 'Run PHPCS only')) }} + if: ${{ (github.event_name != 'workflow_dispatch') || ((github.event.inputs.jobs != 'Run PHPCS only') && (github.event.inputs.jobs != 'Run lint only') && (github.event.inputs.jobs != 'Run unit tests only')) }} uses: inpsyde/reusable-workflows/.github/workflows/static-analysis-php.yml@main + strategy: + matrix: + php: [ '7.4', '8.0', '8.1', '8.2', '8.3' ] + with: + PHP_VERSION: ${{ matrix.php }} + PSALM_ARGS: '--no-suggestions --report-show-info=false --find-unused-psalm-suppress --no-diff --no-cache --no-file-cache --long-progress' tests-unit-php: if: ${{ (github.event_name != 'workflow_dispatch') || ((github.event.inputs.jobs == 'Run all') || (github.event.inputs.jobs == 'Run unit tests only')) }} uses: inpsyde/reusable-workflows/.github/workflows/tests-unit-php.yml@main strategy: matrix: - php: ["7.1", "7.2", "7.3", "7.4", "8.0", "8.1"] + php: [ '7.4', '8.0', '8.1', '8.2', '8.3' ] with: PHP_VERSION: ${{ matrix.php }} + PHPUNIT_ARGS: '--no-coverage' diff --git a/.psalm/autoloader.php b/.psalm/autoloader.php deleted file mode 100644 index 3592d19..0000000 --- a/.psalm/autoloader.php +++ /dev/null @@ -1,17 +0,0 @@ -is(WpContext::INSTALLING)` / `->isInstalling()` - `->is(WpContext::WP_ACTIVATE)` / `->isWpActivate()` + + ### About "core" and "installing" contexts `WpContext::isCore()` checks for the constants `ABSPATH` being defined, which means that it will normally be true when all the check for other contexts is also true, but `WpContext::isInstalling()` is an exception to that (more on this below). @@ -73,6 +77,8 @@ if (Inpsyde\WpContext::determine()->isCore()) { which might look very fine, could break if `WP_INSTALLING` is true, considering in that case the options table might not be there at all. Thanks to the fact that `WpContext::isCore()` returns false when `WP_INSTALLING` is true the `get_option` call above is not executed during installation (when it is not safe to call). + + ### About "installing" and "activate" contexts The previous section states: @@ -140,6 +146,13 @@ Note that `$context->force(WpContext::CLI)` can still be used to "simulate" requ +## Requirements + +- WordPress 6.1+ +- PHP 7.4+ + + + ## Crafted by Inpsyde The team at [Inpsyde](https://inpsyde.com) is engineering the Web since 2006. @@ -148,7 +161,7 @@ The team at [Inpsyde](https://inpsyde.com) is engineering the Web since 2006. ## License -Copyright (c) 2020 Inpsyde GmbH +Copyright (c) 2024 Inpsyde GmbH This library is released under ["GPL 2.0 or later" License](LICENSE). diff --git a/composer.json b/composer.json index e96ed0d..e7617c9 100644 --- a/composer.json +++ b/composer.json @@ -16,16 +16,26 @@ "role": "Developer" } ], + "repositories": [ + { + "type": "composer", + "url": "https://raw.githubusercontent.com/inpsyde/wp-stubs/main", + "only": [ + "inpsyde/wp-stubs-versions" + ] + } + ], "require": { - "php": ">=7.1" + "php": ">=7.4 < 8.4", + "ext-json": "*" }, "require-dev": { - "phpunit/phpunit": "~7.5.20 || ^8", - "inpsyde/php-coding-standards": "^1", - "vimeo/psalm": "^4.27.0", - "mockery/mockery": "~1.3.6", + "phpunit/phpunit": "^9.6.17", + "inpsyde/php-coding-standards": "^2@dev", + "vimeo/psalm": "^5.22.2", "brain/monkey": "^2.6.1", - "inpsyde/wp-stubs": "dev-main" + "roots/wordpress-no-content": ">=6.1", + "inpsyde/wp-stubs-versions": "dev-latest" }, "autoload": { "psr-4": { @@ -44,10 +54,8 @@ "psalm": "@php ./vendor/vimeo/psalm/psalm --no-cache --output-format=compact", "tests": "@php ./vendor/phpunit/phpunit/phpunit", "tests:no-cov": "@php ./vendor/phpunit/phpunit/phpunit --no-coverage", - "phpcompat": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs -p . --standard=PHPCompatibility --ignore=*/vendor/* --extensions=php --basepath=./ --runtime-set testVersion 7.1-", "qa": [ "@cs", - "@phpcompat", "@psalm", "@tests:no-cov" ] diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 6708883..8599be2 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -6,7 +6,7 @@ - + diff --git a/psalm.xml b/psalm.xml index 607e584..80dcd76 100644 --- a/psalm.xml +++ b/psalm.xml @@ -4,16 +4,15 @@ useDocblockPropertyTypes="true" usePhpDocMethodsWithoutMagicCall="true" strictBinaryOperands="true" - ignoreInternalFunctionFalseReturn="false" - ignoreInternalFunctionNullReturn="false" + findUnusedBaselineEntry="true" + findUnusedCode="false" hideExternalErrors="true" - allowNamedArgumentCalls="false" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" > - + diff --git a/src/WpContext.php b/src/WpContext.php index 0962c16..9bc3e51 100644 --- a/src/WpContext.php +++ b/src/WpContext.php @@ -18,6 +18,7 @@ class WpContext implements \JsonSerializable public const XML_RPC = 'xml-rpc'; public const WP_ACTIVATE = 'wp-activate'; + private const ACTIONS_PRIORITY = -300982; private const ALL = [ self::AJAX, self::BACKOFFICE, @@ -32,15 +33,11 @@ class WpContext implements \JsonSerializable self::WP_ACTIVATE, ]; - /** - * @var array - */ - private $data; + /** @var array, bool> */ + private array $data; - /** - * @var array - */ - private $actionCallbacks = []; + /** @var array */ + private array $actionCallbacks = []; /** * @return WpContext @@ -107,9 +104,10 @@ final public static function determine(): WpContext */ private static function isRestRequest(): bool { + /** @psalm-suppress RedundantCondition */ if ( - (defined('REST_REQUEST') && REST_REQUEST) - || !empty($_GET['rest_route']) // phpcs:ignore + (defined('REST_REQUEST') && \REST_REQUEST) + || (isset($_GET['rest_route']) && (bool) $_GET['rest_route']) // phpcs:ignore ) { return true; } @@ -127,8 +125,11 @@ private static function isRestRequest(): bool $GLOBALS['wp_rewrite'] = new \WP_Rewrite(); } - $currentPath = trim((string)parse_url((string)add_query_arg([]), PHP_URL_PATH), '/') . '/'; - $restPath = trim((string)parse_url((string)get_rest_url(), PHP_URL_PATH), '/') . '/'; + $currentUrl = (string) parse_url((string) add_query_arg([]), PHP_URL_PATH); + $currentPath = trim($currentUrl, '/') . '/'; + + $restUrlPath = (string) parse_url((string) get_rest_url(), PHP_URL_PATH); + $restPath = trim($restUrlPath, '/') . '/'; return strpos($currentPath, $restPath) === 0; } @@ -138,28 +139,7 @@ private static function isRestRequest(): bool */ private static function isLoginRequest(): bool { - /** - * New core function with WordPress 6.1 - * @link https://make.wordpress.org/core/2022/09/11/new-is_login-function-for-determining-if-a-page-is-the-login-screen/ - */ - if (function_exists('is_login')) { - return is_login() !== false; - } - - if (!empty($_REQUEST['interim-login'])) { // phpcs:ignore - return true; - } - - /** - * Fallback and 1:1 copy from is_login() in case, the function is - * not available for WP < 6.1. - * phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotValidated - * phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash - * phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - */ - $scriptName = $_SERVER['SCRIPT_NAME'] ?? ''; - - return false !== stripos(wp_login_url(), $scriptName); + return is_login() !== false; } /** @@ -177,19 +157,19 @@ private static function isWpActivateRequest(): bool */ private static function isPageNow(string $page, string $url): bool { - $pageNow = (string)($GLOBALS['pagenow'] ?? ''); + $pageNow = (string) ($GLOBALS['pagenow'] ?? ''); if ($pageNow && (basename($pageNow) === $page)) { return true; } - $currentPath = (string)parse_url(add_query_arg([]), PHP_URL_PATH); - $targetPath = (string)parse_url($url, PHP_URL_PATH); + $currentPath = (string) parse_url(add_query_arg([]), PHP_URL_PATH); + $targetPath = (string) parse_url($url, PHP_URL_PATH); return trim($currentPath, '/') === trim($targetPath, '/'); } /** - * @param array $data + * @param array, bool> $data */ private function __construct(array $data) { @@ -203,7 +183,7 @@ private function __construct(array $data) final public function force(string $context): WpContext { if (!in_array($context, self::ALL, true)) { - throw new \LogicException("'{$context}' is not a valid context."); + throw new \LogicException(esc_html("'{$context}' is not a valid context.")); } $this->removeActionHooks(); @@ -336,7 +316,7 @@ public function isWpActivate(): bool } /** - * @return array + * @return array, bool> */ public function jsonSerialize(): array { @@ -372,7 +352,7 @@ private function addActionHooks(): void ]; foreach ($this->actionCallbacks as $action => $callback) { - add_action($action, $callback, PHP_INT_MIN); + add_action($action, $callback, self::ACTIONS_PRIORITY); } } @@ -385,7 +365,7 @@ private function addActionHooks(): void private function removeActionHooks(): void { foreach ($this->actionCallbacks as $action => $callback) { - remove_action($action, $callback, PHP_INT_MIN); + remove_action($action, $callback, self::ACTIONS_PRIORITY); } $this->actionCallbacks = []; } diff --git a/tests/WpContextTest.php b/tests/WpContextTest.php index b03a650..cc03745 100644 --- a/tests/WpContextTest.php +++ b/tests/WpContextTest.php @@ -16,10 +16,7 @@ class WpContextTest extends TestCase { use MockeryPHPUnitIntegration; - /** - * @var string - */ - private $currentPath = '/'; + private string $currentPath = '/'; /** * @return void @@ -107,9 +104,11 @@ public function testIsLoginLate(): void $onLoginInit = null; Monkey\Actions\expectAdded('login_init') - ->whenHappen(static function (callable $callback) use (&$onLoginInit) { - $onLoginInit = $callback; - }); + ->whenHappen( + static function (callable $callback) use (&$onLoginInit): void { + $onLoginInit = $callback; + } + ); $context = WpContext::determine(); @@ -163,9 +162,11 @@ public function testIsRestLate(): void $onRestInit = null; Monkey\Actions\expectAdded('rest_api_init') - ->whenHappen(static function (callable $callback) use (&$onRestInit) { - $onRestInit = $callback; - }); + ->whenHappen( + static function (callable $callback) use (&$onRestInit): void { + $onRestInit = $callback; + } + ); $context = WpContext::determine(); @@ -374,9 +375,11 @@ public function testIsWpActivateLate(): void $onActivateHeader = null; Monkey\Actions\expectAdded('activate_header') - ->whenHappen(static function (callable $callback) use (&$onActivateHeader) { - $onActivateHeader = $callback; - }); + ->whenHappen( + static function (callable $callback) use (&$onActivateHeader): void { + $onActivateHeader = $callback; + } + ); $context = WpContext::determine(); @@ -431,7 +434,7 @@ public function testJsonSerialize(): void $this->mockIsLoginRequest(true); $context = WpContext::determine(); - $decoded = (array)json_decode((string)json_encode($context), true); + $decoded = (array) json_decode((string) json_encode($context), true); static::assertTrue($decoded[WpContext::CORE]); static::assertTrue($decoded[WpContext::LOGIN]); @@ -445,6 +448,7 @@ public function testJsonSerialize(): void /** * @param bool $is + * @return void */ private function mockIsRestRequest(bool $is): void { @@ -456,6 +460,7 @@ private function mockIsRestRequest(bool $is): void /** * @param bool $is + * @return void */ private function mockIsLoginRequest(bool $is): void { @@ -465,6 +470,7 @@ private function mockIsLoginRequest(bool $is): void /** * @param bool $is + * @return void */ private function mockIsActivateRequest(bool $is): void {