From 9b699d97bd0d96e39ee1f8f62741ac927bb89843 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Fri, 13 Jun 2025 16:47:41 -0600 Subject: [PATCH 1/4] feat: adds phpstan support --- composer.json | 10 ++- composer.lock | 180 ++++++++++++++++++++++++++++++++++++++++++++++++-- phpstan.neon | 26 ++++++++ 3 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 phpstan.neon diff --git a/composer.json b/composer.json index 5e9ace7..a234679 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,9 @@ "lucatume/di52": "^3.0", "lucatume/wp-browser": "^3.0.14", "codeception/module-phpbrowser": "^1.0.0", - "codeception/module-asserts": "^1.0.0" + "codeception/module-asserts": "^1.0.0", + "phpstan/phpstan": "^1.12", + "szepeviktor/phpstan-wordpress": "^1.3" }, "autoload": { "psr-4": { @@ -45,5 +47,9 @@ "type": "git", "url": "https://github.com/wordpress/wordpress-develop.git" } - ] + ], + "scripts": { + "phpstan": "phpstan analyse --memory-limit=512M", + "phpstan:baseline": "phpstan analyse --generate-baseline=phpstan-baseline.neon --memory-limit=512M" + } } diff --git a/composer.lock b/composer.lock index 429a4ec..c0f19ea 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2784a3dc6410135037ac4c4588d24cd6", + "content-hash": "e030b86c9bdc981348f98418ad2c2e39", "packages": [ { "name": "stellarwp/container-contract", @@ -1894,6 +1894,57 @@ }, "time": "2017-03-05T17:38:23+00:00" }, + { + "name": "php-stubs/wordpress-stubs", + "version": "v6.8.1", + "source": { + "type": "git", + "url": "https://github.com/php-stubs/wordpress-stubs.git", + "reference": "92e444847d94f7c30f88c60004648f507688acd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/92e444847d94f7c30f88c60004648f507688acd5", + "reference": "92e444847d94f7c30f88c60004648f507688acd5", + "shasum": "" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "5.6.1" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "nikic/php-parser": "^5.4", + "php": "^7.4 || ^8.0", + "php-stubs/generator": "^0.8.3", + "phpdocumentor/reflection-docblock": "^5.4.1", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9.5", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "paragonie/sodium_compat": "Pure PHP implementation of libsodium", + "symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress function and class declaration stubs for static analysis.", + "homepage": "https://github.com/php-stubs/wordpress-stubs", + "keywords": [ + "PHPStan", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/php-stubs/wordpress-stubs/issues", + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.8.1" + }, + "time": "2025-05-02T12:33:34+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -2121,6 +2172,64 @@ }, "time": "2020-03-05T15:02:03+00:00" }, + { + "name": "phpstan/phpstan", + "version": "1.12.27", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "3a6e423c076ab39dfedc307e2ac627ef579db162" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/3a6e423c076ab39dfedc307e2ac627ef579db162", + "reference": "3a6e423c076ab39dfedc307e2ac627ef579db162", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-05-21T20:51:45+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "5.3.2", @@ -4851,6 +4960,69 @@ ], "time": "2022-08-02T15:47:23+00:00" }, + { + "name": "szepeviktor/phpstan-wordpress", + "version": "v1.3.5", + "source": { + "type": "git", + "url": "https://github.com/szepeviktor/phpstan-wordpress.git", + "reference": "7f8cfe992faa96b6a33bbd75c7bace98864161e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/7f8cfe992faa96b6a33bbd75c7bace98864161e7", + "reference": "7f8cfe992faa96b6a33bbd75c7bace98864161e7", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "php-stubs/wordpress-stubs": "^4.7 || ^5.0 || ^6.0", + "phpstan/phpstan": "^1.10.31", + "symfony/polyfill-php73": "^1.12.0" + }, + "require-dev": { + "composer/composer": "^2.1.14", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpstan/phpstan-strict-rules": "^1.2", + "phpunit/phpunit": "^8.0 || ^9.0", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.0", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "swissspidy/phpstan-no-private": "Detect usage of internal core functions, classes and methods" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "SzepeViktor\\PHPStan\\WordPress\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress extensions for PHPStan", + "keywords": [ + "PHPStan", + "code analyse", + "code analysis", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.3.5" + }, + "time": "2024-06-28T22:27:19+00:00" + }, { "name": "theseer/tokenizer", "version": "1.2.1", @@ -5247,15 +5419,15 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { "ext-json": "*" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "7.2" }, - "plugin-api-version": "2.0.0" + "plugin-api-version": "2.6.0" } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..5264ea6 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,26 @@ +includes: + - vendor/szepeviktor/phpstan-wordpress/extension.neon + +parameters: + # Level 5 is a good balance between strictness and practicality + level: 5 + + # Paths to analyze + paths: + - src + + # Exclude paths + excludePaths: + - vendor/* + - _wordpress/* + - _plugins/* + + # Ignore specific errors if needed + ignoreErrors: + # Add specific error patterns to ignore here if needed + + # PHP version (must be an integer) + phpVersion: 70200 + + # Treat missing return types as mixed + treatPhpDocTypesAsCertain: false From a735850def14860a497cdd384d0948ed83aecdf5 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Fri, 13 Jun 2025 16:48:00 -0600 Subject: [PATCH 2/4] fix: corrects issues caught by phpstan --- src/Config.php | 4 +++- .../Contracts/ValidationExceptionInterface.php | 4 +++- src/Rules/Abstracts/ConditionalRule.php | 1 + src/Rules/DateTime.php | 1 + src/ValidationRuleSet.php | 17 +++++++++-------- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/Config.php b/src/Config.php index 1fa76a3..73cd527 100644 --- a/src/Config.php +++ b/src/Config.php @@ -82,6 +82,7 @@ public static function setHookPrefix(string $prefix) /** * @since 1.0.0 * + * @return never * @throws ValidationExceptionInterface */ public static function throwValidationException() @@ -118,6 +119,7 @@ public static function setValidationExceptionClass(string $validationExceptionCl /** * @since 1.0.0 * + * @return never * @throws InvalidArgumentException */ public static function throwInvalidArgumentException() @@ -162,7 +164,7 @@ public static function initialize() return; } - if (empty(self::$container)) { + if (self::$container === null) { throw new RuntimeException('A service container must be set before initializing the library'); } diff --git a/src/Exceptions/Contracts/ValidationExceptionInterface.php b/src/Exceptions/Contracts/ValidationExceptionInterface.php index 8e07a6f..9b49854 100644 --- a/src/Exceptions/Contracts/ValidationExceptionInterface.php +++ b/src/Exceptions/Contracts/ValidationExceptionInterface.php @@ -4,7 +4,9 @@ namespace StellarWP\Validation\Exceptions\Contracts; -interface ValidationExceptionInterface +use Throwable; + +interface ValidationExceptionInterface extends Throwable { } diff --git a/src/Rules/Abstracts/ConditionalRule.php b/src/Rules/Abstracts/ConditionalRule.php index e17450c..f1b3d17 100644 --- a/src/Rules/Abstracts/ConditionalRule.php +++ b/src/Rules/Abstracts/ConditionalRule.php @@ -58,6 +58,7 @@ public static function fromString(string $options = null): ValidationRule $conditionSet->and($rule[0], '=', $rule[1]); } + // @phpstan-ignore-next-line return new static($conditionSet); } diff --git a/src/Rules/DateTime.php b/src/Rules/DateTime.php index c46d2bb..2a8a735 100644 --- a/src/Rules/DateTime.php +++ b/src/Rules/DateTime.php @@ -35,6 +35,7 @@ public static function id(): string */ public static function fromString(string $options = null): ValidationRule { + // @phpstan-ignore-next-line return new static($options); } diff --git a/src/ValidationRuleSet.php b/src/ValidationRuleSet.php index 49f9923..e06b158 100644 --- a/src/ValidationRuleSet.php +++ b/src/ValidationRuleSet.php @@ -153,7 +153,7 @@ public function getRule(string $rule) * * @since 1.0.0 * - * @return void + * @return self */ public function removeRuleWithId(string $id): self { @@ -258,14 +258,14 @@ private function sanitizeRule($rule) return $rule; } elseif (is_string($rule)) { return $this->getRuleFromString($rule); - } else { - Config::throwInvalidArgumentException( - sprintf( - 'Validation rule must be a string, instance of %s, or a closure', - ValidationRule::class - ) - ); } + + Config::throwInvalidArgumentException( + sprintf( + 'Validation rule must be a string, instance of %s, or a closure', + ValidationRule::class + ) + ); } /** @@ -277,6 +277,7 @@ private function sanitizeRule($rule) */ private function validateClosureRule(Closure $closure) { + $reflection = null; try { $reflection = new ReflectionFunction($closure); } catch (ReflectionException $e) { From 7db4e6b2317a6e8940840e1abf70b0f959adc242 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Fri, 13 Jun 2025 16:48:24 -0600 Subject: [PATCH 3/4] chore: adds phpstan workflow --- .github/workflows/phpstan.yml | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/phpstan.yml diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..6a1bdf3 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,44 @@ +name: 'PHPStan Static Analysis' + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.2' + coverage: none + tools: composer:v2 + + - name: Get Composer Cache Directory + id: composer-cache + run: | + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress --no-suggest --no-interaction + + - name: Run PHPStan + run: composer phpstan From c1e8f26cc2e5a9fe8cee00f5aac06ab04978cc51 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 16 Jun 2025 11:29:44 -0600 Subject: [PATCH 4/4] refactor: updates to require PHP 7.4 --- .github/workflows/phpstan.yml | 2 +- composer.json | 2 +- phpstan.neon | 4 ++-- src/Config.php | 8 ++++---- src/Contracts/ValidatesOnFrontEnd.php | 2 +- src/Contracts/ValidationRule.php | 5 +++++ src/Rules/Abstracts/ConditionalRule.php | 8 ++++---- src/Rules/In.php | 8 ++++++-- src/ServiceProvider.php | 7 +++++-- src/ValidationRuleSet.php | 12 +++++++----- src/ValidationRulesRegistrar.php | 6 ++++-- src/Validator.php | 23 +++++++++++++---------- 12 files changed, 53 insertions(+), 34 deletions(-) diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 6a1bdf3..513d14d 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -20,7 +20,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '7.2' + php-version: '7.4' coverage: none tools: composer:v2 diff --git a/composer.json b/composer.json index a234679..7dd17fa 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ }, "config": { "platform": { - "php": "7.2" + "php": "7.4" } }, "repositories": [ diff --git a/phpstan.neon b/phpstan.neon index 5264ea6..1712066 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,7 +3,7 @@ includes: parameters: # Level 5 is a good balance between strictness and practicality - level: 5 + level: 6 # Paths to analyze paths: @@ -20,7 +20,7 @@ parameters: # Add specific error patterns to ignore here if needed # PHP version (must be an integer) - phpVersion: 70200 + phpVersion: 70400 # Treat missing return types as mixed treatPhpDocTypesAsCertain: false diff --git a/src/Config.php b/src/Config.php index 73cd527..2147e66 100644 --- a/src/Config.php +++ b/src/Config.php @@ -48,7 +48,7 @@ class Config * * @param ContainerInterface $container */ - public static function setServiceContainer($container) + public static function setServiceContainer($container): void { self::$container = $container; } @@ -74,7 +74,7 @@ public static function getHookPrefix(): string /** * @since 1.0.0 */ - public static function setHookPrefix(string $prefix) + public static function setHookPrefix(string $prefix): void { self::$hookPrefix = $prefix; } @@ -105,7 +105,7 @@ public static function getValidationExceptionClass(): string * * @param class-string $validationExceptionClass */ - public static function setValidationExceptionClass(string $validationExceptionClass) + public static function setValidationExceptionClass(string $validationExceptionClass): void { if (!is_a($validationExceptionClass, ValidationExceptionInterface::class, true)) { throw new RuntimeException( @@ -142,7 +142,7 @@ public static function getInvalidArgumentExceptionClass(): string * * @param class-string $invalidArgumentExceptionClass */ - public static function setInvalidArgumentExceptionClass(string $invalidArgumentExceptionClass) + public static function setInvalidArgumentExceptionClass(string $invalidArgumentExceptionClass): void { if (!is_a($invalidArgumentExceptionClass, InvalidArgumentException::class, true)) { throw new RuntimeException( diff --git a/src/Contracts/ValidatesOnFrontEnd.php b/src/Contracts/ValidatesOnFrontEnd.php index 428c97c..8795519 100644 --- a/src/Contracts/ValidatesOnFrontEnd.php +++ b/src/Contracts/ValidatesOnFrontEnd.php @@ -11,7 +11,7 @@ interface ValidatesOnFrontEnd * * @since 1.0.0 * - * @return int|float|string|bool|array|null + * @return mixed */ public function serializeOption(); } diff --git a/src/Contracts/ValidationRule.php b/src/Contracts/ValidationRule.php index 1753b60..86e27ea 100644 --- a/src/Contracts/ValidationRule.php +++ b/src/Contracts/ValidationRule.php @@ -34,6 +34,11 @@ public static function fromString(string $options = null): ValidationRule; * @since 1.2.0 add ExcludeValue return option * @since 1.0.0 * + * @param mixed $value + * @param Closure $fail + * @param string $key + * @param array $values + * * @return void|ExcludeValue|SkipValidationRules */ public function __invoke($value, Closure $fail, string $key, array $values); diff --git a/src/Rules/Abstracts/ConditionalRule.php b/src/Rules/Abstracts/ConditionalRule.php index f1b3d17..94b3024 100644 --- a/src/Rules/Abstracts/ConditionalRule.php +++ b/src/Rules/Abstracts/ConditionalRule.php @@ -5,9 +5,9 @@ namespace StellarWP\Validation\Rules\Abstracts; use StellarWP\FieldConditions\ComplexConditionSet; +use StellarWP\FieldConditions\SimpleConditionSet; use StellarWP\FieldConditions\Contracts\Condition; use StellarWP\FieldConditions\Contracts\ConditionSet; -use StellarWP\FieldConditions\SimpleConditionSet; use StellarWP\Validation\Config; use StellarWP\Validation\Contracts\ValidatesOnFrontEnd; use StellarWP\Validation\Contracts\ValidationRule; @@ -15,12 +15,12 @@ abstract class ConditionalRule implements ValidationRule, ValidatesOnFrontEnd { /** - * @var ConditionSet + * @var SimpleConditionSet|ComplexConditionSet */ - protected $conditions; + protected ConditionSet $conditions; /** - * @param ConditionSet|Condition[] $conditions + * @param SimpleConditionSet|ComplexConditionSet|Condition[] $conditions */ public function __construct($conditions) { diff --git a/src/Rules/In.php b/src/Rules/In.php index 12fb241..50ca7d5 100644 --- a/src/Rules/In.php +++ b/src/Rules/In.php @@ -10,9 +10,9 @@ class In implements ValidationRule, ValidatesOnFrontEnd { /** - * @var array + * @var array */ - protected $acceptedValues; + protected array $acceptedValues; /** * @since 1.2.0 @@ -24,6 +24,8 @@ public static function id(): string /** * @since 1.2.0 + * + * @param mixed ...$acceptedValues */ final public function __construct(...$acceptedValues) { @@ -64,6 +66,8 @@ public function __invoke($value, Closure $fail, string $key, array $values) /** * @since 1.2.0 + * + * @return array */ public function serializeOption(): array { diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 9a0a043..93a86e6 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -28,7 +28,10 @@ class ServiceProvider { - private $validationRules = [ + /** + * @var array + */ + private array $validationRules = [ Required::class, Min::class, Max::class, @@ -55,7 +58,7 @@ class ServiceProvider /** * Registers the validation rules registrar with the container */ - public function register() + public function register(): void { Config::getServiceContainer()->singleton(ValidationRulesRegistrar::class, function () { $register = new ValidationRulesRegistrar(); diff --git a/src/ValidationRuleSet.php b/src/ValidationRuleSet.php index e06b158..ff09f27 100644 --- a/src/ValidationRuleSet.php +++ b/src/ValidationRuleSet.php @@ -15,17 +15,17 @@ use StellarWP\Validation\Contracts\ValidationRule; use Traversable; +/** + * @implements IteratorAggregate + */ class ValidationRuleSet implements IteratorAggregate, JsonSerializable { - /** - * @var ValidationRulesRegistrar - */ - private $register; + private ValidationRulesRegistrar $register; /** * @var array */ - private $rules = []; + private array $rules = []; /** * @since 1.0.0 @@ -168,6 +168,8 @@ public function removeRuleWithId(string $id): self * Returns the validation rules. * * @since 1.0.0 + * + * @return array */ public function getRules(): array { diff --git a/src/ValidationRulesRegistrar.php b/src/ValidationRulesRegistrar.php index 92109d9..5cceda1 100644 --- a/src/ValidationRulesRegistrar.php +++ b/src/ValidationRulesRegistrar.php @@ -11,8 +11,10 @@ */ class ValidationRulesRegistrar { - /** @var array */ - protected $rules = []; + /** + * @var array> + */ + protected array $rules = []; /** * Register one or many validation rules. diff --git a/src/Validator.php b/src/Validator.php index 8c22e9f..fc727f6 100644 --- a/src/Validator.php +++ b/src/Validator.php @@ -7,6 +7,8 @@ use StellarWP\Validation\Commands\ExcludeValue; use StellarWP\Validation\Commands\SkipValidationRules; use StellarWP\Validation\Contracts\Sanitizer; +use StellarWP\Validation\Contracts\ValidationRule; +use Closure; /** * A tool for taking in a set of values and corresponding validation rules, and then validating the values. @@ -18,38 +20,39 @@ class Validator /** * @var array */ - private $ruleSets; + private array $ruleSets; /** * @var array */ - private $values; + private array $values; /** * @var array */ - private $labels; + private array $labels; /** * @var array */ - private $errors = []; + private array $errors = []; /** * @var array */ - private $validatedValues = []; + private array $validatedValues = []; /** * @var bool */ - private $ranValidationRules = false; + private bool $ranValidationRules = false; /** * @since 1.0.0 * - * @param array $ruleSets + * @param array> $ruleSets * @param array $values + * @param array $labels */ public function __construct(array $ruleSets, array $values, array $labels = []) { @@ -98,10 +101,8 @@ public function passes(): bool * Will run only once, and then store the results for subsequent calls. * * @since 1.0.0 - * - * @return void */ - private function runValidationRules() + private function runValidationRules(): void { if ($this->ranValidationRules) { return; @@ -156,6 +157,8 @@ public function errors(): array * Returns the validated values, with any sanitization rules applied. * * @since 1.0.0 + * + * @return array */ public function validated(): array {