From eceaecd8ff877db4eedbb1047f7a3e4b711734a0 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Sat, 16 May 2026 00:41:06 +0900 Subject: [PATCH 1/8] chore: make CSRF protection explicit and add project checks --- .github/dependabot.yml | 13 ++ .github/workflows/coding-standards.yml | 45 ++++++ .github/workflows/continuous-integration.yml | 56 ++++++++ .github/workflows/static-analysis.yml | 130 ++++++++++++++++++ .php_cs => .php-cs-fixer.php | 38 ++--- CHANGELOG.md | 56 ++++++++ README.JA.md | 17 ++- README.md | 21 ++- composer-require-checker.json | 13 ++ composer.json | 19 ++- docs/demo/1.csrf/ContactForm.php | 1 + docs/demo/1.csrf/Controller.php | 8 +- docs/demo/1.csrf/MyModule.php | 1 + docs/demo/1.csrf/run.php | 1 + docs/demo/1.csrf/web.php | 1 + docs/demo/autoload.php | 1 + phpstan.neon | 23 ++++ psalm.xml | 31 +++++ src/AbstractForm.php | 89 ++++++++---- src/Annotation/AbstractValidation.php | 1 - src/Annotation/CsrfProtection.php | 17 +++ src/Annotation/FormValidation.php | 2 - src/Annotation/InputValidation.php | 1 - src/Annotation/VndError.php | 4 +- src/AntiCsrf.php | 11 +- src/AuraInputInterceptor.php | 90 +++++++----- src/AuraInputModule.php | 4 +- src/Exception/CsrfViolationException.php | 1 - src/Exception/ExceptionInterface.php | 1 - src/Exception/InvalidArgumentException.php | 1 - .../InvalidFormPropertyException.php | 1 - src/Exception/InvalidOnFailureMethod.php | 1 - src/Exception/LogicException.php | 1 - src/Exception/RuntimeException.php | 1 - src/Exception/ValidationException.php | 5 +- src/FailureHandlerInterface.php | 8 +- src/FormFactory.php | 15 +- src/FormInterface.php | 6 +- src/FormValidationError.php | 10 +- src/FormVndErrorModule.php | 1 - src/InputValidationInterceptor.php | 3 +- src/OnFailureMethodHandler.php | 19 ++- src/SetAntiCsrfTrait.php | 3 +- src/SubmitInterface.php | 3 +- src/ToStringInterface.php | 3 +- src/VndErrorHandler.php | 31 +++-- tests/AbstractAuraFormTest.php | 3 +- tests/AbstractFormTest.php | 3 +- tests/AntiCsrfTest.php | 3 +- tests/AuraInputInterceptorTest.php | 16 ++- tests/AuraInputModuleTest.php | 1 + tests/Fake/FakeCsrfController.php | 37 +++++ tests/FormFactoryTest.php | 3 +- tests/VndErrorHandlerTest.php | 3 +- tests/bootstrap.php | 1 + 55 files changed, 716 insertions(+), 162 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/coding-standards.yml create mode 100644 .github/workflows/continuous-integration.yml create mode 100644 .github/workflows/static-analysis.yml rename .php_cs => .php-cs-fixer.php (77%) create mode 100644 CHANGELOG.md create mode 100644 composer-require-checker.json create mode 100644 phpstan.neon create mode 100644 psalm.xml create mode 100644 src/Annotation/CsrfProtection.php create mode 100644 tests/Fake/FakeCsrfController.php diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b0a252d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "weekly" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml new file mode 100644 index 0000000..b0bf7f1 --- /dev/null +++ b/.github/workflows/coding-standards.yml @@ -0,0 +1,45 @@ +name: Coding Standards + +on: + push: + paths-ignore: + - '**.md' + pull_request: + paths-ignore: + - '**.md' + workflow_dispatch: + inputs: + php_version: + default: '8.4' + +jobs: + coding-standards: + name: Coding Standards + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ inputs.php_version || '8.4' }} + tools: cs2pr + coverage: none + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Run PHP_CodeSniffer + run: ./vendor/bin/phpcs -q --no-colors --report=checkstyle --standard=./phpcs.xml src | cs2pr diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml new file mode 100644 index 0000000..47ee88f --- /dev/null +++ b/.github/workflows/continuous-integration.yml @@ -0,0 +1,56 @@ +name: Continuous Integration + +on: + push: + paths-ignore: + - '**.md' + pull_request: + paths-ignore: + - '**.md' + workflow_dispatch: + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + operating-system: + - ubuntu-latest + php-version: + - '8.0' + - '8.1' + - '8.2' + - '8.3' + - '8.4' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP ${{ matrix.php-version }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + coverage: none + tools: none + ini-values: assert.exception=1, zend.assertions=1 + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-php${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + ${{ runner.os }}-php${{ matrix.php-version }}-composer- + ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer update --no-interaction --no-progress --prefer-dist + + - name: Run test suite + run: ./vendor/bin/phpunit diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 0000000..4af412a --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,130 @@ +name: Static Analysis + +on: + push: + paths-ignore: + - '**.md' + pull_request: + paths-ignore: + - '**.md' + workflow_dispatch: + inputs: + php_version: + default: '8.4' + +jobs: + static-analysis-phpstan: + name: PHPStan + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ inputs.php_version }} + tools: cs2pr + coverage: none + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache 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 --no-interaction --no-progress --prefer-dist + + - name: Run PHPStan + run: ./vendor/bin/phpstan analyse -c phpstan.neon --no-progress --no-interaction --error-format=checkstyle | cs2pr + + static-analysis-psalm: + name: Psalm + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ inputs.php_version }} + tools: cs2pr + coverage: none + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Install dependencies + run: composer install --no-interaction --no-progress --prefer-dist + + - name: Run Psalm + run: ./vendor/bin/psalm --show-info=false --output-format=checkstyle --shepherd | cs2pr + + static-analysis-phpmd: + name: PHPMD + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ inputs.php_version }} + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache 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 --no-interaction --no-progress --prefer-dist + + - name: Run PHP Mess Detector + run: ./vendor/bin/phpmd src text ./phpmd.xml + + static-analysis-composer-require-checker: + name: ComposerRequireChecker + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ inputs.php_version }} + coverage: none + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Install dependencies + run: | + composer install --no-interaction --no-progress --prefer-dist + + - name: Run composer-require-checker with config + run: ./vendor/bin/composer-require-checker check ./composer.json --config-file=./composer-require-checker.json diff --git a/.php_cs b/.php-cs-fixer.php similarity index 77% rename from .php_cs rename to .php-cs-fixer.php index 3518e7e..5d7ee7d 100644 --- a/.php_cs +++ b/.php-cs-fixer.php @@ -6,16 +6,16 @@ @license http://opensource.org/licenses/MIT MIT EOF; -return \PhpCsFixer\Config::create() +return (new \PhpCsFixer\Config()) ->setRiskyAllowed(true) ->setRules(array( '@PSR2' => true, - 'header_comment' => ['header' => $header, 'commentType' => 'PHPDoc', 'separate' => 'none'], + 'header_comment' => ['header' => $header, 'comment_type' => 'PHPDoc', 'separate' => 'none'], 'array_syntax' => ['syntax' => 'short'], - 'binary_operator_spaces' => ['align_equals' => false, 'align_double_arrow' => false], + 'binary_operator_spaces' => ['default' => 'single_space'], 'blank_line_after_opening_tag' => true, 'blank_line_after_namespace' => false, - 'blank_line_before_return' => true, + 'blank_line_before_statement' => ['statements' => ['return']], 'cast_spaces' => true, // 'class_keyword_remove' => true, 'combine_consecutive_unsets' => true, @@ -24,41 +24,40 @@ 'declare_strict_types' => false, 'dir_constant' => true, 'ereg_to_preg' => true, - 'function_typehint_space' => true, + 'type_declaration_spaces' => true, 'general_phpdoc_annotation_remove' => true, - 'hash_to_slash_comment' => true, + 'single_line_comment_style' => ['comment_types' => ['hash']], 'heredoc_to_nowdoc' => true, 'include' => true, 'indentation_type' => true, - 'is_null' => ['use_yoda_style' => false], + 'is_null' => true, 'linebreak_after_opening_tag' => true, 'lowercase_cast' => true, // 'mb_str_functions' => true, - 'method_separation' => true, + 'class_attributes_separation' => ['elements' => ['method' => 'one', 'trait_import' => 'none']], 'modernize_types_casting' => true, 'native_function_casing' => true, // 'native_function_invocation' => true, - 'new_with_braces' => false, // + 'new_with_parentheses' => false, // 'no_alias_functions' => true, 'no_blank_lines_after_class_opening' => true, 'no_blank_lines_after_phpdoc' => true, - 'no_blank_lines_before_namespace' => true, + 'blank_lines_before_namespace' => ['min_line_breaks' => 1, 'max_line_breaks' => 1], 'no_empty_comment' => true, 'no_empty_phpdoc' => true, 'no_empty_statement' => true, - 'no_extra_consecutive_blank_lines' => ['break', 'continue', 'curly_brace_block', 'extra', 'parenthesis_brace_block', 'return', 'square_brace_block', 'throw', 'use', 'useTrait'], + 'no_extra_blank_lines' => ['tokens' => ['break', 'continue', 'curly_brace_block', 'extra', 'parenthesis_brace_block', 'return', 'square_brace_block', 'throw', 'use']], 'no_leading_import_slash' => true, 'no_leading_namespace_whitespace' => true, 'no_mixed_echo_print' => ['use' => 'echo'], 'no_multiline_whitespace_around_double_arrow' => true, - 'no_multiline_whitespace_before_semicolons' => true, + 'multiline_whitespace_before_semicolons' => ['strategy' => 'no_multi_line'], 'no_php4_constructor' => false, 'no_short_bool_cast' => true, - 'no_short_echo_tag' => false, + 'echo_tag_syntax' => false, 'no_singleline_whitespace_before_semicolons' => true, 'no_spaces_around_offset' => true, - 'no_trailing_comma_in_list_call' => true, - 'no_trailing_comma_in_singleline_array' => true, + 'no_trailing_comma_in_singleline' => true, 'no_trailing_whitespace' => true, 'no_trailing_whitespace_in_comment' => true, 'no_unneeded_control_parentheses' => true, @@ -82,9 +81,9 @@ 'phpdoc_align' => true, 'phpdoc_annotation_without_dot' => true, 'phpdoc_indent' => true, - 'phpdoc_inline_tag' => true, + 'phpdoc_inline_tag_normalizer' => true, 'phpdoc_no_access' => true, - 'phpdoc_no_alias_tag' => ['property-read' => 'property', 'property-write' => 'property', 'type' => 'var'], + 'phpdoc_no_alias_tag' => ['replacements' => ['property-read' => 'property', 'property-write' => 'property', 'type' => 'var']], 'phpdoc_no_empty_return' => true, 'phpdoc_no_package' => true, // 'phpdoc_no_useless_inheritdoc' => true, @@ -101,8 +100,7 @@ 'pow_to_exponentiation' => true, // 'pre_increment' => true, 'protected_to_private' => true, - 'psr0' => true, - 'psr4' => true, + 'psr_autoloading' => true, 'random_api_migration' => true, 'return_type_declaration' => ['space_before' => 'one'], 'self_accessor' => true, @@ -125,8 +123,10 @@ ->setFinder( PhpCsFixer\Finder::create() ->exclude('tests/Fake') + ->exclude('tests/tmp') ->exclude('src-data') ->exclude('src-deprecated') + ->exclude('docs/demo/tmp') ->in(__DIR__) )->setLineEnding("\n") ->setUsingCache(false); diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d9625f4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,56 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - unreleased + +### Changed + +- **BC break**: Minimum PHP version raised to `8.0`. +- **BC break**: Migrated from Doctrine Annotations to PHP 8 Attributes. All + validation metadata (`@FormValidation`, `@InputValidation`, `@VndError`) is + now expressed with `#[FormValidation]`, `#[InputValidation]`, `#[VndError]`. +- CSRF protection for validation methods is declared with the separate + `#[CsrfProtection]` attribute instead of a boolean option on + `#[FormValidation]`. +- **BC break**: `AuraInputInterceptor`, `InputValidationInterceptor` and + `VndErrorHandler` no longer accept a `Doctrine\Common\Annotations\Reader` + in their constructors. Validation attributes are read directly via + `ReflectionMethod::getAttributes()`. +- **BC break**: `FormInterface::input()` and `FormInterface::error()` now declare + parameter and return types (`string $input`, `: string` respectively). + Implementations must update their signatures. +- Added property type declarations and return types across the codebase to + align with PHP 8 typing. +- Bumped dependencies: `ray/di` `^2.16`, `ray/aop` `^2.14`, + `phpunit/phpunit` `^9.5`. + +### Fixed + +- `Exception\RuntimeException` now correctly extends `\RuntimeException` + instead of `\LogicException`. +- `AntiCsrf::isValid()` uses strict comparison for the CSRF token. +- Eliminated PHP 8.4 deprecation warnings for implicit nullable parameters in + `ValidationException::__construct()` and `VndErrorHandler::makeVndError()`. + +### Added + +- GitHub Actions workflows for tests and coding standards. +- `CHANGELOG.md`. +- `#[CsrfProtection]` attribute for composing CSRF checks with form/input + validation attributes. + +### Removed + +- `doctrine/annotations` dependency. +- Travis CI configuration; replaced with GitHub Actions. + +## [0.6.0] - 2018-05-27 + +See git history for changes prior to 1.0.0. + +[1.0.0]: https://github.com/ray-di/Ray.WebFormModule/compare/0.6.0...1.0.0 +[0.6.0]: https://github.com/ray-di/Ray.WebFormModule/releases/tag/0.6.0 diff --git a/README.JA.md b/README.JA.md index edcdbb8..4dac375 100644 --- a/README.JA.md +++ b/README.JA.md @@ -1,8 +1,8 @@ # Ray.WebFormModule +[![Continuous Integration](https://github.com/ray-di/Ray.WebFormModule/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/ray-di/Ray.WebFormModule/actions/workflows/continuous-integration.yml) +[![Coding Standards](https://github.com/ray-di/Ray.WebFormModule/actions/workflows/coding-standards.yml/badge.svg)](https://github.com/ray-di/Ray.WebFormModule/actions/workflows/coding-standards.yml) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/ray-di/Ray.WebFormModule/badges/quality-score.png?b=1.x)](https://scrutinizer-ci.com/g/ray-di/Ray.WebFormModule/?branch=1.x) -[![Code Coverage](https://scrutinizer-ci.com/g/ray-di/Ray.WebFormModule/badges/coverage.png?b=1.x)](https://scrutinizer-ci.com/g/ray-di/Ray.WebFormModule/?branch=1.x) -[![Build Status](https://travis-ci.org/ray-di/Ray.WebFormModule.svg?branch=1.x)](https://travis-ci.org/ray-di/Ray.WebFormModule) Ray.WebFormModuleはアスペクト指向でフォームのバリデーションを行うモジュールです。 フォームライブラリには[Aura.Input](https://github.com/auraphp/Aura.Input)を使い、 @@ -127,11 +127,24 @@ class MyController CSRF対策を行うためにはフォームにCSRFオブジェクトをセットします。 ```php +use Ray\WebFormModule\AbstractAuraForm; +use Ray\WebFormModule\Annotation\CsrfProtection; +use Ray\WebFormModule\Annotation\FormValidation; use Ray\WebFormModule\SetAntiCsrfTrait; class MyForm extends AbstractAuraForm { use SetAntiCsrfTrait; +} + +class MyController +{ + #[FormValidation(form: "contactForm")] + #[CsrfProtection] + public function createAction() + { + } +} ``` セキュリティレベルを高めるためにはユーザーの認証を含んだカスタムCsrfクラスを作成してフォームクラスにセットします。 diff --git a/README.md b/README.md index 36740e2..933efe4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Ray.WebFormModule +[![Continuous Integration](https://github.com/ray-di/Ray.WebFormModule/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/ray-di/Ray.WebFormModule/actions/workflows/continuous-integration.yml) +[![Coding Standards](https://github.com/ray-di/Ray.WebFormModule/actions/workflows/coding-standards.yml/badge.svg)](https://github.com/ray-di/Ray.WebFormModule/actions/workflows/coding-standards.yml) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/ray-di/Ray.WebFormModule/badges/quality-score.png?b=1.x)](https://scrutinizer-ci.com/g/ray-di/Ray.WebFormModule/?branch=1.x) -[![Code Coverage](https://scrutinizer-ci.com/g/ray-di/Ray.WebFormModule/badges/coverage.png?b=1.x)](https://scrutinizer-ci.com/g/ray-di/Ray.WebFormModule/?branch=1.x) -[![Build Status](https://travis-ci.org/ray-di/Ray.WebFormModule.svg?branch=1.x)](https://travis-ci.org/ray-di/Ray.WebFormModule) An aspect oriented web form module powered by [Aura.Input](https://github.com/auraphp/Aura.Input) and [Ray.Di](https://github.com/ray-di/Ray.Di). @@ -12,7 +12,7 @@ An aspect oriented web form module powered by [Aura.Input](https://github.com/au ### Composer install - $ composer require web-form-module + $ composer require ray/web-form-module ### Module install @@ -139,11 +139,24 @@ or render input element basis. ## CSRF Protections ```php +use Ray\WebFormModule\AbstractAuraForm; +use Ray\WebFormModule\Annotation\CsrfProtection; +use Ray\WebFormModule\Annotation\FormValidation; use Ray\WebFormModule\SetAntiCsrfTrait; -class MyController +class MyForm extends AbstractAuraForm { use SetAntiCsrfTrait; +} + +class MyController +{ + #[FormValidation(form: "contactForm")] + #[CsrfProtection] + public function createAction() + { + } +} ``` You can provide your custom `AntiCsrf` class. See more detail at [Aura.Input](https://github.com/auraphp/Aura.Input#applying-csrf-protections) diff --git a/composer-require-checker.json b/composer-require-checker.json new file mode 100644 index 0000000..2c1e5fd --- /dev/null +++ b/composer-require-checker.json @@ -0,0 +1,13 @@ +{ + "symbol-whitelist": [], + "php-core-extensions": [ + "Core", + "date", + "json", + "pcre", + "Reflection", + "SPL", + "standard" + ], + "scan-files": [] +} diff --git a/composer.json b/composer.json index e36bd59..77ec358 100644 --- a/composer.json +++ b/composer.json @@ -12,10 +12,17 @@ "aura/input": "^1.2", "aura/filter": "^2.3|3.x-dev", "aura/html": "^2.5", + "aura/session": "^2.1 || ^4.0", "ray/aura-session-module": "^1.1" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^9.5", + "friendsofphp/php-cs-fixer": "^3.0", + "phpstan/phpstan": "^1.10", + "squizlabs/php_codesniffer": "^3.7", + "vimeo/psalm": "^5.0 || ^6.0", + "phpmd/phpmd": "^2.13", + "maglnet/composer-require-checker": "^4.0" }, "license": "MIT", "autoload":{ @@ -30,9 +37,15 @@ }, "scripts" :{ "test": ["@cs", "phpunit"], + "tests": ["@cs", "@sa", "@test"], "coverage": ["php -dzend_extension=xdebug.so ./vendor/bin/phpunit --coverage-text --coverage-html=build/coverage"], - "cs": ["php-cs-fixer fix -v --dry-run", "phpcs --standard=./phpcs.xml src"], - "cs-fix": ["php-cs-fixer fix -v", "phpcbf src"] + "cs": ["php-cs-fixer fix -v --dry-run --sequential", "phpcs --standard=./phpcs.xml src"], + "cs-fix": ["php-cs-fixer fix -v --sequential", "phpcbf src"], + "phpstan": "phpstan analyse -c phpstan.neon --no-progress", + "psalm": "psalm --show-info=false", + "phpmd": "phpmd src text ./phpmd.xml", + "crc": "composer-require-checker check ./composer.json --config-file=./composer-require-checker.json", + "sa": ["@phpstan", "@psalm"] }, "config": { "allow-plugins": { diff --git a/docs/demo/1.csrf/ContactForm.php b/docs/demo/1.csrf/ContactForm.php index ce91165..fead5aa 100644 --- a/docs/demo/1.csrf/ContactForm.php +++ b/docs/demo/1.csrf/ContactForm.php @@ -1,4 +1,5 @@ response['code'] = 201; diff --git a/docs/demo/1.csrf/MyModule.php b/docs/demo/1.csrf/MyModule.php index 295da43..a687cb5 100644 --- a/docs/demo/1.csrf/MyModule.php +++ b/docs/demo/1.csrf/MyModule.php @@ -1,4 +1,5 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AbstractForm.php b/src/AbstractForm.php index 2274021..430390f 100644 --- a/src/AbstractForm.php +++ b/src/AbstractForm.php @@ -7,31 +7,36 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule; use ArrayIterator; +use function assert; use Aura\Filter\FilterFactory; use Aura\Filter\SubjectFilter; use Aura\Html\HelperLocator; use Aura\Html\HelperLocatorFactory; use Aura\Input\AntiCsrfInterface; +use Aura\Input\Builder; use Aura\Input\BuilderInterface; use Aura\Input\Fieldset; use Exception; +use function is_string; use Ray\Di\Di\Inject; use Ray\Di\Di\PostConstruct; use Ray\WebFormModule\Exception\CsrfViolationException; use Ray\WebFormModule\Exception\LogicException; - +use Stringable; use function trigger_error; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ abstract class AbstractForm extends Fieldset implements FormInterface { /** @var SubjectFilter */ protected $filter; - /** @var array>|null */ + /** @var array>|null */ protected ?array $errorMessages = null; protected HelperLocator $helper; @@ -50,10 +55,8 @@ public function __clone() /** * Return form markup string - * - * @return string */ - public function __toString() + public function __toString() : string { try { if (! $this instanceof ToStringInterface) { @@ -65,7 +68,8 @@ public function __toString() trigger_error($e->getMessage() . PHP_EOL . $e->getTraceAsString(), E_USER_ERROR); } - return ''; + // Reachable when a custom error handler intercepts E_USER_ERROR without halting. + return ''; // @phpstan-ignore deadCode.unreachable } /** @@ -78,40 +82,59 @@ public function setBaseDependencies( BuilderInterface $builder, FilterFactory $filterFactory, HelperLocatorFactory $helperFactory - ): void { + ) : void { + assert($builder instanceof Builder); $this->builder = $builder; $this->filter = $filterFactory->newSubjectFilter(); $this->helper = $helperFactory->newInstance(); } - public function setAntiCsrf(AntiCsrfInterface $antiCsrf): void + public function setAntiCsrf(AntiCsrfInterface $antiCsrf) : void + { + $this->antiCsrf = $antiCsrf; + } + + public function enableAntiCsrf(AntiCsrfInterface $antiCsrf) : void { $this->antiCsrf = $antiCsrf; + if (isset($this->inputs[AntiCsrf::TOKEN_KEY])) { + return; + } + + $this->antiCsrf->setField($this); } #[PostConstruct] - public function postConstruct(): void + public function postConstruct() : void { $this->init(); if ($this->antiCsrf instanceof AntiCsrfInterface) { - $this->antiCsrf->setField($this); + $this->enableAntiCsrf($this->antiCsrf); } } /** {@inheritdoc} */ - public function input($input) + public function input(string $input) : string { - return $this->helper->input($this->get($input)); + $inputHtml = $this->helper->input($this->get($input)); + assert(is_string($inputHtml) || $inputHtml instanceof Stringable); + + return (string) $inputHtml; } /** {@inheritdoc} */ - public function error(string $input): string + public function error(string $input) : string { - if (! $this->errorMessages) { + if ($this->errorMessages === null) { + /** @var \Aura\Filter\Failure\FailureCollection|null $failure */ $failure = $this->filter->getFailures(); - if ($failure) { - $this->errorMessages = $failure->getMessages(); + if ($failure === null) { + return ''; } + + /** @var array> $messages */ + $messages = $failure->getMessages(); + $this->errorMessages = $messages; } if (isset($this->errorMessages[$input])) { @@ -122,16 +145,19 @@ public function error(string $input): string } /** - * @param array $attr attributes for the form tag + * @param array $attr attributes for the form tag * * @throws \Aura\Input\Exception\NoSuchInput * @throws \Aura\Html\Exception\HelperNotFound */ - public function form(array $attr = []): string + public function form(array $attr = []) : string { + /** @var string $form */ $form = $this->helper->form($attr); if (isset($this->inputs['__csrf_token'])) { - $form .= $this->helper->input($this->get('__csrf_token')); + /** @var string $input */ + $input = $this->helper->input($this->get('__csrf_token')); + $form .= $input; } return $form; @@ -140,14 +166,14 @@ public function form(array $attr = []): string /** * Applies the filter to a subject. * - * @param array $data + * @param array $data * * @throws CsrfViolationException */ - public function apply(array $data): bool + public function apply(array $data) : bool { if ($this->antiCsrf && ! $this->antiCsrf->isValid($data)) { - throw new CsrfViolationException; + throw new CsrfViolationException(); } $this->fill($data); @@ -158,15 +184,22 @@ public function apply(array $data): bool /** * Returns all failure messages for all fields. * - * @return list + * @return array> */ - public function getFailureMessages(): array + public function getFailureMessages() : array { - return $this->filter->getFailures()->getMessages(); + /** @var array> $messages */ + $messages = $this->filter->getFailures()->getMessages(); + + return $messages; } - /** Returns all the fields collection */ - public function getIterator(): ArrayIterator + /** + * Returns all the fields collection + * + * @return ArrayIterator + */ + public function getIterator() : ArrayIterator { return new ArrayIterator($this->inputs); } diff --git a/src/Annotation/AbstractValidation.php b/src/Annotation/AbstractValidation.php index b866cf3..4249912 100644 --- a/src/Annotation/AbstractValidation.php +++ b/src/Annotation/AbstractValidation.php @@ -7,7 +7,6 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule\Annotation; abstract class AbstractValidation diff --git a/src/Annotation/CsrfProtection.php b/src/Annotation/CsrfProtection.php new file mode 100644 index 0000000..ba786fa --- /dev/null +++ b/src/Annotation/CsrfProtection.php @@ -0,0 +1,17 @@ + $href + * @param string|null $logref + * @param string|null $path * * @see http://www.w3.org/TR/html5/links.html#link-type-help * @see http://tools.ietf.org/html/rfc6903#section-2 diff --git a/src/AntiCsrf.php b/src/AntiCsrf.php index bec9e6a..3d9a9b0 100644 --- a/src/AntiCsrf.php +++ b/src/AntiCsrf.php @@ -7,15 +7,12 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule; use Aura\Input\AntiCsrfInterface; use Aura\Input\Fieldset; use Aura\Session\Session; - use function is_bool; - use const PHP_SAPI; final class AntiCsrf implements AntiCsrfInterface @@ -34,14 +31,14 @@ public function __construct(Session $session, bool|null $isCli = null) $this->isCli = is_bool($isCli) ? $isCli : PHP_SAPI === 'cli'; } - public function setField(Fieldset $fieldset): void + public function setField(Fieldset $fieldset) : void { $fieldset->setField(self::TOKEN_KEY, 'hidden') ->setAttribs(['value' => $this->getToken()]); } - /** @param array $data */ - public function isValid(array $data): bool + /** @param array $data */ + public function isValid(array $data) : bool { if ($this->isCli) { return true; @@ -50,7 +47,7 @@ public function isValid(array $data): bool return isset($data[self::TOKEN_KEY]) && $data[self::TOKEN_KEY] === $this->getToken(); } - private function getToken(): string + private function getToken() : string { return $this->isCli ? self::TEST_TOKEN : $this->session->getCsrfToken()->getValue(); } diff --git a/src/AuraInputInterceptor.php b/src/AuraInputInterceptor.php index 697693a..e2368af 100644 --- a/src/AuraInputInterceptor.php +++ b/src/AuraInputInterceptor.php @@ -7,33 +7,47 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule; +use function array_shift; +use Aura\Input\AntiCsrfInterface; +use function property_exists; use Ray\Aop\MethodInterceptor; use Ray\Aop\MethodInvocation; +use Ray\Di\Di\Inject; use Ray\WebFormModule\Annotation\AbstractValidation; +use Ray\WebFormModule\Annotation\CsrfProtection; use Ray\WebFormModule\Exception\InvalidArgumentException; use Ray\WebFormModule\Exception\InvalidFormPropertyException; use ReflectionAttribute; use ReflectionClass; use ReflectionMethod; -use function array_shift; -use function property_exists; - +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class AuraInputInterceptor implements MethodInterceptor { protected FailureHandlerInterface $failureHandler; + private AntiCsrfInterface|null $antiCsrf = null; + public function __construct(FailureHandlerInterface $handler) { $this->failureHandler = $handler; } + #[Inject] + public function setAntiCsrf(AntiCsrfInterface $antiCsrf) : void + { + $this->antiCsrf = $antiCsrf; + } + /** * {@inheritdoc} * + * @param MethodInvocation $invocation + * * @throws InvalidArgumentException */ public function invoke(MethodInvocation $invocation) @@ -45,8 +59,11 @@ public function invoke(MethodInvocation $invocation) } $form = $this->getFormProperty($formValidation, $object); + $this->enableCsrfProtection($invocation->getMethod(), $form); $data = $form instanceof SubmitInterface ? $form->submit() : $this->getNamedArguments($invocation); - $isValid = $this->isValid($data, $form); + /** @var array $submit */ + $submit = (array) $data; + $isValid = $this->isValid($submit, $form); if ($isValid === true) { return $invocation->proceed(); } @@ -54,40 +71,52 @@ public function invoke(MethodInvocation $invocation) return $this->failureHandler->handle($formValidation, $invocation, $form); } - private function getValidationAttribute(ReflectionMethod $method): AbstractValidation|null + /** + * @param array $submit + * + * @throws Exception\CsrfViolationException + */ + public function isValid(array $submit, AbstractForm $form) : bool { - $attributes = $method->getAttributes(AbstractValidation::class, ReflectionAttribute::IS_INSTANCEOF); - if ($attributes === []) { - return null; + return $form->apply($submit); + } + + /** + * @throws InvalidArgumentException + */ + private function enableCsrfProtection(ReflectionMethod $method, AbstractForm $form) : void + { + if ($method->getAttributes(CsrfProtection::class) === []) { + return; } - $instance = $attributes[0]->newInstance(); - assert($instance instanceof AbstractValidation); + if (! $this->antiCsrf instanceof AntiCsrfInterface) { + throw new InvalidArgumentException('#[CsrfProtection] requires AntiCsrfInterface'); + } - return $instance; + $form->enableAntiCsrf($this->antiCsrf); } - /** - * @param array $submit - * @param AbstractForm $form - * - * @return bool - * @throws Exception\CsrfViolationException - * - */ - public function isValid(array $submit, AbstractForm $form): bool + private function getValidationAttribute(ReflectionMethod $method) : AbstractValidation|null { - return $form->apply($submit); + $attributes = $method->getAttributes(AbstractValidation::class, ReflectionAttribute::IS_INSTANCEOF); + if ($attributes === []) { + return null; + } + + return $attributes[0]->newInstance(); } /** * Return arguments as named arguments. * - * @param MethodInvocation $invocation + * @param MethodInvocation $invocation * - * @return array + * @return array + * + * @SuppressWarnings(PHPMD.Superglobals) */ - private function getNamedArguments(MethodInvocation $invocation): array + private function getNamedArguments(MethodInvocation $invocation) : array { $submit = []; $params = $invocation->getMethod()->getParameters(); @@ -97,7 +126,6 @@ private function getNamedArguments(MethodInvocation $invocation): array $submit[$param->getName()] = $arg; } - // has token? if (isset($_POST[AntiCsrf::TOKEN_KEY])) { $submit[AntiCsrf::TOKEN_KEY] = $_POST[AntiCsrf::TOKEN_KEY]; } @@ -105,15 +133,7 @@ private function getNamedArguments(MethodInvocation $invocation): array return $submit; } - /** - * Return form property - * - * @param AbstractValidation $formValidation - * @param object $object - * - * @return mixed - */ - private function getFormProperty(AbstractValidation $formValidation, $object) + private function getFormProperty(AbstractValidation $formValidation, object $object) : AbstractForm { if (! property_exists($object, $formValidation->form)) { throw new InvalidFormPropertyException($formValidation->form); diff --git a/src/AuraInputModule.php b/src/AuraInputModule.php index d2f56d6..803d9ea 100644 --- a/src/AuraInputModule.php +++ b/src/AuraInputModule.php @@ -7,7 +7,6 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule; use Aura\Filter\FilterFactory; @@ -23,6 +22,9 @@ use Ray\WebFormModule\Annotation\FormValidation; use Ray\WebFormModule\Annotation\InputValidation; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class AuraInputModule extends AbstractModule { /** {@inheritdoc} */ diff --git a/src/Exception/CsrfViolationException.php b/src/Exception/CsrfViolationException.php index c97b0b3..816cc28 100644 --- a/src/Exception/CsrfViolationException.php +++ b/src/Exception/CsrfViolationException.php @@ -7,7 +7,6 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule\Exception; use Aura\Input\Exception\CsrfViolation; diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php index 237ca9d..dd435dc 100644 --- a/src/Exception/ExceptionInterface.php +++ b/src/Exception/ExceptionInterface.php @@ -7,7 +7,6 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule\Exception; interface ExceptionInterface diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php index 5afa83d..47323d4 100644 --- a/src/Exception/InvalidArgumentException.php +++ b/src/Exception/InvalidArgumentException.php @@ -7,7 +7,6 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule\Exception; class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface diff --git a/src/Exception/InvalidFormPropertyException.php b/src/Exception/InvalidFormPropertyException.php index a17cfc6..fcb28e0 100644 --- a/src/Exception/InvalidFormPropertyException.php +++ b/src/Exception/InvalidFormPropertyException.php @@ -7,7 +7,6 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule\Exception; class InvalidFormPropertyException extends LogicException diff --git a/src/Exception/InvalidOnFailureMethod.php b/src/Exception/InvalidOnFailureMethod.php index c4f0ea2..1e0c20e 100644 --- a/src/Exception/InvalidOnFailureMethod.php +++ b/src/Exception/InvalidOnFailureMethod.php @@ -7,7 +7,6 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule\Exception; class InvalidOnFailureMethod extends LogicException diff --git a/src/Exception/LogicException.php b/src/Exception/LogicException.php index d5ec12c..da9cee0 100644 --- a/src/Exception/LogicException.php +++ b/src/Exception/LogicException.php @@ -7,7 +7,6 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule\Exception; class LogicException extends \LogicException implements ExceptionInterface diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php index fe827e8..345d956 100644 --- a/src/Exception/RuntimeException.php +++ b/src/Exception/RuntimeException.php @@ -7,7 +7,6 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule\Exception; class RuntimeException extends \RuntimeException implements ExceptionInterface diff --git a/src/Exception/ValidationException.php b/src/Exception/ValidationException.php index 1ed951d..aa557b0 100644 --- a/src/Exception/ValidationException.php +++ b/src/Exception/ValidationException.php @@ -7,7 +7,6 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule\Exception; use Exception; @@ -15,9 +14,9 @@ class ValidationException extends Exception { - public $error; + public ?FormValidationError $error; - public function __construct($message = '', $code = 0, ?Exception $e = null, ?FormValidationError $error = null) + public function __construct(string $message = '', int $code = 0, ?Exception $e = null, ?FormValidationError $error = null) { parent::__construct($message, $code, $e); $this->error = $error; diff --git a/src/FailureHandlerInterface.php b/src/FailureHandlerInterface.php index 948becf..67c0eb0 100644 --- a/src/FailureHandlerInterface.php +++ b/src/FailureHandlerInterface.php @@ -7,7 +7,6 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule; use Ray\Aop\MethodInvocation; @@ -15,5 +14,12 @@ interface FailureHandlerInterface { + /** + * @param AbstractValidation $formValidation + * @param MethodInvocation $invocation + * @param AbstractForm $form + * + * @return mixed + */ public function handle(AbstractValidation $formValidation, MethodInvocation $invocation, AbstractForm $form); } diff --git a/src/FormFactory.php b/src/FormFactory.php index ed12a80..ae72d72 100644 --- a/src/FormFactory.php +++ b/src/FormFactory.php @@ -7,7 +7,6 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule; use Aura\Filter\FilterFactory; @@ -16,11 +15,17 @@ final class FormFactory { - public function newInstance(string $class): AbstractForm + /** + * @param string $class + * + * @phpstan-param class-string $class + * + * @psalm-param class-string $class + */ + public function newInstance(string $class) : AbstractForm { - /** @var $form AbstractForm */ - $form = new $class; - $form->setBaseDependencies(new Builder, new FilterFactory, new HelperLocatorFactory); + $form = new $class(); + $form->setBaseDependencies(new Builder(), new FilterFactory(), new HelperLocatorFactory()); $form->postConstruct(); return $form; diff --git a/src/FormInterface.php b/src/FormInterface.php index 5e36178..ab0e9e0 100644 --- a/src/FormInterface.php +++ b/src/FormInterface.php @@ -7,7 +7,6 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule; interface FormInterface @@ -15,11 +14,12 @@ interface FormInterface /** * Return input element html * - * @return string * @throws \Aura\Input\Exception\NoSuchInput + * + * @return string */ public function input(string $input); /** Return error message */ - public function error(string $input): string; + public function error(string $input) : string; } diff --git a/src/FormValidationError.php b/src/FormValidationError.php index 6cdec60..75b95b8 100644 --- a/src/FormValidationError.php +++ b/src/FormValidationError.php @@ -7,25 +7,27 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule; use function json_encode; - use const JSON_PRETTY_PRINT; use const JSON_UNESCAPED_SLASHES; class FormValidationError { + /** @var array */ private array $value; + /** + * @param array $value + */ public function __construct(array $value) { $this->value = $value; } - public function __toString() + public function __toString() : string { - return json_encode($this->value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + return (string) json_encode($this->value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); } } diff --git a/src/FormVndErrorModule.php b/src/FormVndErrorModule.php index a4064b2..b8e76f4 100644 --- a/src/FormVndErrorModule.php +++ b/src/FormVndErrorModule.php @@ -7,7 +7,6 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule; use Ray\Di\AbstractModule; diff --git a/src/InputValidationInterceptor.php b/src/InputValidationInterceptor.php index e59098e..fec9ef9 100644 --- a/src/InputValidationInterceptor.php +++ b/src/InputValidationInterceptor.php @@ -7,7 +7,6 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule; use Ray\Di\Di\Named; @@ -15,7 +14,7 @@ class InputValidationInterceptor extends AuraInputInterceptor { public function __construct( - #[Named("vnd_error")] + #[Named('vnd_error')] FailureHandlerInterface $handler, ) { parent::__construct($handler); diff --git a/src/OnFailureMethodHandler.php b/src/OnFailureMethodHandler.php index 77ff764..0a10936 100644 --- a/src/OnFailureMethodHandler.php +++ b/src/OnFailureMethodHandler.php @@ -7,22 +7,26 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule; +use function call_user_func_array; +use function method_exists; use Ray\Aop\MethodInvocation; use Ray\WebFormModule\Annotation\AbstractValidation; use Ray\WebFormModule\Annotation\FormValidation; use Ray\WebFormModule\Exception\InvalidOnFailureMethod; -use function call_user_func_array; -use function method_exists; - final class OnFailureMethodHandler implements FailureHandlerInterface { public const FAILURE_SUFFIX = 'ValidationFailed'; - /** {@inheritdoc} */ + /** + * {@inheritdoc} + * + * @param AbstractValidation $formValidation + * @param MethodInvocation $invocation + * @param AbstractForm $form + */ public function handle(AbstractValidation $formValidation, MethodInvocation $invocation, AbstractForm $form) { unset($form); @@ -37,6 +41,9 @@ public function handle(AbstractValidation $formValidation, MethodInvocation $inv throw new InvalidOnFailureMethod(get_class($invocation->getThis())); } - return call_user_func_array([$invocation->getThis(), $onFailureMethod], $args); + /** @var callable $callable */ + $callable = [$object, $onFailureMethod]; + + return call_user_func_array($callable, $args); } } diff --git a/src/SetAntiCsrfTrait.php b/src/SetAntiCsrfTrait.php index e3fb26b..d746279 100644 --- a/src/SetAntiCsrfTrait.php +++ b/src/SetAntiCsrfTrait.php @@ -7,7 +7,6 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule; use Aura\Input\AntiCsrfInterface; @@ -16,7 +15,7 @@ trait SetAntiCsrfTrait { #[Inject] - public function setAntiCsrf(AntiCsrfInterface $antiCsrf): void + public function setAntiCsrf(AntiCsrfInterface $antiCsrf) : void { $this->antiCsrf = $antiCsrf; } diff --git a/src/SubmitInterface.php b/src/SubmitInterface.php index 10db621..d902e64 100644 --- a/src/SubmitInterface.php +++ b/src/SubmitInterface.php @@ -7,7 +7,6 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule; interface SubmitInterface @@ -15,7 +14,7 @@ interface SubmitInterface /** * Return subject value * - * @return array|object + * @return array|object */ public function submit(); } diff --git a/src/ToStringInterface.php b/src/ToStringInterface.php index 22ed248..3ce4a09 100644 --- a/src/ToStringInterface.php +++ b/src/ToStringInterface.php @@ -7,11 +7,10 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule; /** Return form markup string */ interface ToStringInterface { - public function toString(): string; + public function toString() : string; } diff --git a/src/VndErrorHandler.php b/src/VndErrorHandler.php index 429a6f9..a334af4 100644 --- a/src/VndErrorHandler.php +++ b/src/VndErrorHandler.php @@ -7,7 +7,6 @@ * * @license http://opensource.org/licenses/MIT MIT */ - namespace Ray\WebFormModule; use Ray\Aop\MethodInvocation; @@ -18,7 +17,13 @@ final class VndErrorHandler implements FailureHandlerInterface { - /** {@inheritdoc} */ + /** + * {@inheritdoc} + * + * @param AbstractValidation $formValidation + * @param MethodInvocation $invocation + * @param AbstractForm $form + */ public function handle(AbstractValidation $formValidation, MethodInvocation $invocation, AbstractForm $form) { unset($formValidation); @@ -28,30 +33,34 @@ public function handle(AbstractValidation $formValidation, MethodInvocation $inv throw new ValidationException('Validation failed.', 400, null, $error); } - private function getVndErrorAttribute(ReflectionMethod $method): VndError|null + private function getVndErrorAttribute(ReflectionMethod $method) : VndError|null { $attributes = $method->getAttributes(VndError::class); if ($attributes === []) { return null; } - $instance = $attributes[0]->newInstance(); - assert($instance instanceof VndError); - - return $instance; + return $attributes[0]->newInstance(); } - private function makeVndError(AbstractForm $form, ?VndError $vndError = null) + /** + * @return array + * + * @SuppressWarnings(PHPMD.Superglobals) + */ + private function makeVndError(AbstractForm $form, ?VndError $vndError = null) : array { $body = ['message' => 'Validation failed']; $body['path'] = $_SERVER['PATH_INFO'] ?? ''; $body['validation_messages'] = $form->getFailureMessages(); - $body = $vndError ? $this->optionalAttribute($vndError) + $body : $body; - return $body; + return $vndError ? $this->optionalAttribute($vndError) + $body : $body; } - private function optionalAttribute(VndError $vndError) + /** + * @return array + */ + private function optionalAttribute(VndError $vndError) : array { $body = []; if ($vndError->message) { diff --git a/tests/AbstractAuraFormTest.php b/tests/AbstractAuraFormTest.php index 16862dd..10eba46 100644 --- a/tests/AbstractAuraFormTest.php +++ b/tests/AbstractAuraFormTest.php @@ -1,4 +1,5 @@ form = (new FormFactory)->newInstance(FakeForm::class); diff --git a/tests/AbstractFormTest.php b/tests/AbstractFormTest.php index 86e8263..2e8c9b3 100644 --- a/tests/AbstractFormTest.php +++ b/tests/AbstractFormTest.php @@ -1,4 +1,5 @@ form = (new FormFactory)->newInstance(FakeMiniForm::class); diff --git a/tests/AntiCsrfTest.php b/tests/AntiCsrfTest.php index ebb4297..6b4a6a8 100644 --- a/tests/AntiCsrfTest.php +++ b/tests/AntiCsrfTest.php @@ -1,4 +1,5 @@ phpfunc = new FakePhpfunc; $this->session = $this->newSession(); diff --git a/tests/AuraInputInterceptorTest.php b/tests/AuraInputInterceptorTest.php index 3c1ce68..0b2743e 100644 --- a/tests/AuraInputInterceptorTest.php +++ b/tests/AuraInputInterceptorTest.php @@ -1,4 +1,5 @@ injector = new Injector(new class() extends AbstractModule { protected function configure() { $this->install(new AuraInputModule); $this->bind(FormInterface::class)->annotatedWith('contact_form')->to(FakeForm::class); + $this->bind(FormInterface::class)->annotatedWith('mini_form')->to(FakeMiniForm::class); } }); $this->controller = $this->injector->getInstance(FakeController::class); @@ -49,6 +51,18 @@ public function testProceed() $this->assertSame('201', $result); } + public function testCsrfProtectionAttributeEnablesAntiCsrf() + { + /** @var FakeCsrfController $controller */ + $controller = $this->injector->getInstance(FakeCsrfController::class); + $this->assertStringNotContainsString(AntiCsrf::TOKEN_KEY, $controller->formHtml()); + + $result = $controller->createAction('BEAR'); + + $this->assertSame('201', $result); + $this->assertStringContainsString(AntiCsrf::TOKEN_KEY, $controller->formHtml()); + } + public function testInvalidFormPropertyByMissingProperty() { $this->expectException(InvalidFormPropertyException::class); diff --git a/tests/AuraInputModuleTest.php b/tests/AuraInputModuleTest.php index 47add84..e1d3cfb 100644 --- a/tests/AuraInputModuleTest.php +++ b/tests/AuraInputModuleTest.php @@ -1,4 +1,5 @@ form = $form; + } + + #[FormValidation] + #[CsrfProtection] + public function createAction($name) + { + return '201'; + } + + public function formHtml() : string + { + assert($this->form instanceof AbstractForm); + + return $this->form->form(); + } +} diff --git a/tests/FormFactoryTest.php b/tests/FormFactoryTest.php index c8ae411..e3ecef9 100644 --- a/tests/FormFactoryTest.php +++ b/tests/FormFactoryTest.php @@ -1,4 +1,5 @@ factory = new FormFactory; diff --git a/tests/VndErrorHandlerTest.php b/tests/VndErrorHandlerTest.php index c8b888e..6f350b4 100644 --- a/tests/VndErrorHandlerTest.php +++ b/tests/VndErrorHandlerTest.php @@ -1,4 +1,5 @@ controller = (new Injector(new FakeVndErrorModule, __DIR__ . '/tmp'))->getInstance(FakeController::class); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index adacf3f..039e44c 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,4 +1,5 @@ Date: Sun, 17 May 2026 11:15:52 +0900 Subject: [PATCH 2/8] chore: switch coding standard from PHP-CS-Fixer to Doctrine - Replace friendsofphp/php-cs-fixer with doctrine/coding-standard. - Upgrade squizlabs/php_codesniffer to ^4.0 and rewrite phpcs.xml around PSR12 + Doctrine ruleset with project-specific exclusions. - Drop .php-cs-fixer.php; collapse composer cs/cs-fix scripts to phpcs/phpcbf. - Simplify coding-standards.yml to use the standard auto-discovered ruleset. - Reformat all source, tests and docs to satisfy the new standard (use ordering, attribute syntax, type/throws annotations, doc spacing). --- .github/workflows/coding-standards.yml | 2 +- .gitignore | 1 + .php-cs-fixer.php | 132 ------------------ composer.json | 11 +- docs/demo/1.csrf/ContactForm.php | 6 - docs/demo/1.csrf/Controller.php | 6 - docs/demo/1.csrf/MyModule.php | 6 - docs/demo/1.csrf/run.php | 5 - docs/demo/1.csrf/web.php | 5 - docs/demo/autoload.php | 5 - phpcs.xml | 76 +++++++--- src/AbstractForm.php | 69 ++++----- src/Annotation/AbstractValidation.php | 5 - src/Annotation/CsrfProtection.php | 5 - src/Annotation/FormValidation.php | 7 +- src/Annotation/InputValidation.php | 5 - src/Annotation/VndError.php | 10 +- src/AntiCsrf.php | 18 +-- src/AuraInputInterceptor.php | 33 ++--- src/AuraInputModule.php | 17 +-- src/Exception/CsrfViolationException.php | 5 - src/Exception/ExceptionInterface.php | 5 - src/Exception/InvalidArgumentException.php | 5 - .../InvalidFormPropertyException.php | 5 - src/Exception/InvalidOnFailureMethod.php | 5 - src/Exception/LogicException.php | 5 - src/Exception/RuntimeException.php | 5 - src/Exception/ValidationException.php | 11 +- src/FailureHandlerInterface.php | 7 - src/FormFactory.php | 12 +- src/FormInterface.php | 13 +- src/FormValidationError.php | 18 +-- src/FormVndErrorModule.php | 7 +- src/InputValidationInterceptor.php | 5 - src/OnFailureMethodHandler.php | 15 +- src/SetAntiCsrfTrait.php | 7 +- src/SubmitInterface.php | 5 - src/ToStringInterface.php | 7 +- src/VndErrorHandler.php | 19 +-- tests/AbstractAuraFormTest.php | 38 +++-- tests/AbstractFormTest.php | 48 +++---- tests/AntiCsrfTest.php | 34 ++--- tests/AuraInputInterceptorTest.php | 21 +-- tests/AuraInputModuleTest.php | 13 +- tests/FormFactoryTest.php | 16 +-- tests/VndErrorHandlerTest.php | 20 ++- tests/bootstrap.php | 13 +- 47 files changed, 234 insertions(+), 554 deletions(-) delete mode 100644 .php-cs-fixer.php diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index b0bf7f1..9bd9501 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -42,4 +42,4 @@ jobs: run: composer install --no-interaction --prefer-dist - name: Run PHP_CodeSniffer - run: ./vendor/bin/phpcs -q --no-colors --report=checkstyle --standard=./phpcs.xml src | cs2pr + run: ./vendor/bin/phpcs -q --no-colors --report=checkstyle | cs2pr diff --git a/.gitignore b/.gitignore index b47a5dd..93dec00 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ composer.lock tests/tmp/* !tests/tmp/.placefolder .phpunit.result.cache +.phpcs-cache diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php deleted file mode 100644 index 5d7ee7d..0000000 --- a/.php-cs-fixer.php +++ /dev/null @@ -1,132 +0,0 @@ -setRiskyAllowed(true) - ->setRules(array( - '@PSR2' => true, - 'header_comment' => ['header' => $header, 'comment_type' => 'PHPDoc', 'separate' => 'none'], - 'array_syntax' => ['syntax' => 'short'], - 'binary_operator_spaces' => ['default' => 'single_space'], - 'blank_line_after_opening_tag' => true, - 'blank_line_after_namespace' => false, - 'blank_line_before_statement' => ['statements' => ['return']], - 'cast_spaces' => true, -// 'class_keyword_remove' => true, - 'combine_consecutive_unsets' => true, - 'concat_space' => ['spacing' => 'one'], - 'declare_equal_normalize' => true, - 'declare_strict_types' => false, - 'dir_constant' => true, - 'ereg_to_preg' => true, - 'type_declaration_spaces' => true, - 'general_phpdoc_annotation_remove' => true, - 'single_line_comment_style' => ['comment_types' => ['hash']], - 'heredoc_to_nowdoc' => true, - 'include' => true, - 'indentation_type' => true, - 'is_null' => true, - 'linebreak_after_opening_tag' => true, - 'lowercase_cast' => true, -// 'mb_str_functions' => true, - 'class_attributes_separation' => ['elements' => ['method' => 'one', 'trait_import' => 'none']], - 'modernize_types_casting' => true, - 'native_function_casing' => true, -// 'native_function_invocation' => true, - 'new_with_parentheses' => false, // - 'no_alias_functions' => true, - 'no_blank_lines_after_class_opening' => true, - 'no_blank_lines_after_phpdoc' => true, - 'blank_lines_before_namespace' => ['min_line_breaks' => 1, 'max_line_breaks' => 1], - 'no_empty_comment' => true, - 'no_empty_phpdoc' => true, - 'no_empty_statement' => true, - 'no_extra_blank_lines' => ['tokens' => ['break', 'continue', 'curly_brace_block', 'extra', 'parenthesis_brace_block', 'return', 'square_brace_block', 'throw', 'use']], - 'no_leading_import_slash' => true, - 'no_leading_namespace_whitespace' => true, - 'no_mixed_echo_print' => ['use' => 'echo'], - 'no_multiline_whitespace_around_double_arrow' => true, - 'multiline_whitespace_before_semicolons' => ['strategy' => 'no_multi_line'], - 'no_php4_constructor' => false, - 'no_short_bool_cast' => true, - 'echo_tag_syntax' => false, - 'no_singleline_whitespace_before_semicolons' => true, - 'no_spaces_around_offset' => true, - 'no_trailing_comma_in_singleline' => true, - 'no_trailing_whitespace' => true, - 'no_trailing_whitespace_in_comment' => true, - 'no_unneeded_control_parentheses' => true, - 'no_unreachable_default_argument_value' => true, - 'no_unused_imports' => true, - 'no_useless_else' => true, - 'no_useless_return' => true, - 'no_whitespace_before_comma_in_array' => true, - 'no_whitespace_in_blank_line' => true, - 'normalize_index_brace' => true, - 'not_operator_with_space' => false, - 'not_operator_with_successor_space' => true, - 'object_operator_without_whitespace' => true, - 'ordered_class_elements' => true, - 'ordered_imports' => true, - 'php_unit_construct' => true, - 'php_unit_dedicate_assert' => true, - 'php_unit_fqcn_annotation' => true, - 'php_unit_strict' => true, -// 'phpdoc_add_missing_param_annotation' => true, - 'phpdoc_align' => true, - 'phpdoc_annotation_without_dot' => true, - 'phpdoc_indent' => true, - 'phpdoc_inline_tag_normalizer' => true, - 'phpdoc_no_access' => true, - 'phpdoc_no_alias_tag' => ['replacements' => ['property-read' => 'property', 'property-write' => 'property', 'type' => 'var']], - 'phpdoc_no_empty_return' => true, - 'phpdoc_no_package' => true, -// 'phpdoc_no_useless_inheritdoc' => true, - 'phpdoc_order' => true, - 'phpdoc_return_self_reference' => true, - 'phpdoc_scalar' => true, - 'phpdoc_separation' => true, - 'phpdoc_single_line_var_spacing' => true, -// 'phpdoc_summary' => true, - 'phpdoc_to_comment' => true, - 'phpdoc_trim' => true, - 'phpdoc_types' => true, - 'phpdoc_var_without_name' => true, - 'pow_to_exponentiation' => true, -// 'pre_increment' => true, - 'protected_to_private' => true, - 'psr_autoloading' => true, - 'random_api_migration' => true, - 'return_type_declaration' => ['space_before' => 'one'], - 'self_accessor' => true, - 'short_scalar_cast' => true, -// 'silenced_deprecation_error' => true, -// 'simplified_null_return' => true, -// 'single_blank_line_before_namespace' => true, - 'single_quote' => true, - 'space_after_semicolon' => true, - 'standardize_not_equals' => true, -// 'strict_comparison' => true, - 'ternary_operator_spaces' => true, - 'strict_param' => true, -// 'ternary_to_null_coalescing' => true, -// 'trailing_comma_in_multiline_array' => true, - 'trim_array_spaces' => true, - 'unary_operator_spaces' => true, - 'whitespace_after_comma_in_array' => true - )) - ->setFinder( - PhpCsFixer\Finder::create() - ->exclude('tests/Fake') - ->exclude('tests/tmp') - ->exclude('src-data') - ->exclude('src-deprecated') - ->exclude('docs/demo/tmp') - ->in(__DIR__) - )->setLineEnding("\n") - ->setUsingCache(false); diff --git a/composer.json b/composer.json index 77ec358..6f8b471 100644 --- a/composer.json +++ b/composer.json @@ -17,9 +17,9 @@ }, "require-dev": { "phpunit/phpunit": "^9.5", - "friendsofphp/php-cs-fixer": "^3.0", + "doctrine/coding-standard": "^14.0", "phpstan/phpstan": "^1.10", - "squizlabs/php_codesniffer": "^3.7", + "squizlabs/php_codesniffer": "^4.0", "vimeo/psalm": "^5.0 || ^6.0", "phpmd/phpmd": "^2.13", "maglnet/composer-require-checker": "^4.0" @@ -39,8 +39,8 @@ "test": ["@cs", "phpunit"], "tests": ["@cs", "@sa", "@test"], "coverage": ["php -dzend_extension=xdebug.so ./vendor/bin/phpunit --coverage-text --coverage-html=build/coverage"], - "cs": ["php-cs-fixer fix -v --dry-run --sequential", "phpcs --standard=./phpcs.xml src"], - "cs-fix": ["php-cs-fixer fix -v --sequential", "phpcbf src"], + "cs": "phpcs", + "cs-fix": "phpcbf", "phpstan": "phpstan analyse -c phpstan.neon --no-progress", "psalm": "psalm --show-info=false", "phpmd": "phpmd src text ./phpmd.xml", @@ -49,7 +49,8 @@ }, "config": { "allow-plugins": { - "aura/installer-default": true + "aura/installer-default": true, + "dealerdirect/phpcodesniffer-composer-installer": true } } } diff --git a/docs/demo/1.csrf/ContactForm.php b/docs/demo/1.csrf/ContactForm.php index fead5aa..aea6dbd 100644 --- a/docs/demo/1.csrf/ContactForm.php +++ b/docs/demo/1.csrf/ContactForm.php @@ -1,10 +1,4 @@ addPsr4('Ray\WebFormModule\\', __DIR__); use Aura\Input\Exception\CsrfViolation; diff --git a/docs/demo/1.csrf/web.php b/docs/demo/1.csrf/web.php index dbf88d7..ef72c18 100644 --- a/docs/demo/1.csrf/web.php +++ b/docs/demo/1.csrf/web.php @@ -1,10 +1,5 @@ - - - - - - - - + + + + + + + + + + + + + + + src + tests + */tmp/* + */Fake/* + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AbstractForm.php b/src/AbstractForm.php index 430390f..b5a68f7 100644 --- a/src/AbstractForm.php +++ b/src/AbstractForm.php @@ -2,46 +2,44 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule; use ArrayIterator; -use function assert; +use Aura\Filter\Failure\FailureCollection; use Aura\Filter\FilterFactory; use Aura\Filter\SubjectFilter; +use Aura\Html\Exception\HelperNotFound; use Aura\Html\HelperLocator; use Aura\Html\HelperLocatorFactory; use Aura\Input\AntiCsrfInterface; use Aura\Input\Builder; use Aura\Input\BuilderInterface; +use Aura\Input\Exception\NoSuchInput; use Aura\Input\Fieldset; -use Exception; -use function is_string; use Ray\Di\Di\Inject; use Ray\Di\Di\PostConstruct; use Ray\WebFormModule\Exception\CsrfViolationException; use Ray\WebFormModule\Exception\LogicException; use Stringable; +use Throwable; + +use function assert; +use function is_string; use function trigger_error; -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ +use const E_USER_ERROR; +use const PHP_EOL; + +/** @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ abstract class AbstractForm extends Fieldset implements FormInterface { /** @var SubjectFilter */ protected $filter; /** @var array>|null */ - protected ?array $errorMessages = null; - + protected array|null $errorMessages = null; protected HelperLocator $helper; - - protected ?AntiCsrfInterface $antiCsrf = null; + protected AntiCsrfInterface|null $antiCsrf = null; public function __construct() { @@ -56,7 +54,7 @@ public function __clone() /** * Return form markup string */ - public function __toString() : string + public function __toString(): string { try { if (! $this instanceof ToStringInterface) { @@ -64,7 +62,7 @@ public function __toString() : string } return $this->toString(); - } catch (Exception $e) { + } catch (Throwable $e) { trigger_error($e->getMessage() . PHP_EOL . $e->getTraceAsString(), E_USER_ERROR); } @@ -72,29 +70,24 @@ public function __toString() : string return ''; // @phpstan-ignore deadCode.unreachable } - /** - * @param BuilderInterface $builder - * @param FilterFactory $filterFactory - * @param HelperLocatorFactory $helperFactory - */ #[Inject] public function setBaseDependencies( BuilderInterface $builder, FilterFactory $filterFactory, - HelperLocatorFactory $helperFactory - ) : void { + HelperLocatorFactory $helperFactory, + ): void { assert($builder instanceof Builder); $this->builder = $builder; $this->filter = $filterFactory->newSubjectFilter(); $this->helper = $helperFactory->newInstance(); } - public function setAntiCsrf(AntiCsrfInterface $antiCsrf) : void + public function setAntiCsrf(AntiCsrfInterface $antiCsrf): void { $this->antiCsrf = $antiCsrf; } - public function enableAntiCsrf(AntiCsrfInterface $antiCsrf) : void + public function enableAntiCsrf(AntiCsrfInterface $antiCsrf): void { $this->antiCsrf = $antiCsrf; if (isset($this->inputs[AntiCsrf::TOKEN_KEY])) { @@ -105,7 +98,7 @@ public function enableAntiCsrf(AntiCsrfInterface $antiCsrf) : void } #[PostConstruct] - public function postConstruct() : void + public function postConstruct(): void { $this->init(); if ($this->antiCsrf instanceof AntiCsrfInterface) { @@ -113,8 +106,8 @@ public function postConstruct() : void } } - /** {@inheritdoc} */ - public function input(string $input) : string + /** {@inheritDoc} */ + public function input(string $input): string { $inputHtml = $this->helper->input($this->get($input)); assert(is_string($inputHtml) || $inputHtml instanceof Stringable); @@ -122,11 +115,11 @@ public function input(string $input) : string return (string) $inputHtml; } - /** {@inheritdoc} */ - public function error(string $input) : string + /** {@inheritDoc} */ + public function error(string $input): string { if ($this->errorMessages === null) { - /** @var \Aura\Filter\Failure\FailureCollection|null $failure */ + /** @var FailureCollection|null $failure */ $failure = $this->filter->getFailures(); if ($failure === null) { return ''; @@ -147,10 +140,10 @@ public function error(string $input) : string /** * @param array $attr attributes for the form tag * - * @throws \Aura\Input\Exception\NoSuchInput - * @throws \Aura\Html\Exception\HelperNotFound + * @throws NoSuchInput + * @throws HelperNotFound */ - public function form(array $attr = []) : string + public function form(array $attr = []): string { /** @var string $form */ $form = $this->helper->form($attr); @@ -170,7 +163,7 @@ public function form(array $attr = []) : string * * @throws CsrfViolationException */ - public function apply(array $data) : bool + public function apply(array $data): bool { if ($this->antiCsrf && ! $this->antiCsrf->isValid($data)) { throw new CsrfViolationException(); @@ -186,7 +179,7 @@ public function apply(array $data) : bool * * @return array> */ - public function getFailureMessages() : array + public function getFailureMessages(): array { /** @var array> $messages */ $messages = $this->filter->getFailures()->getMessages(); @@ -199,7 +192,7 @@ public function getFailureMessages() : array * * @return ArrayIterator */ - public function getIterator() : ArrayIterator + public function getIterator(): ArrayIterator { return new ArrayIterator($this->inputs); } diff --git a/src/Annotation/AbstractValidation.php b/src/Annotation/AbstractValidation.php index 4249912..8669808 100644 --- a/src/Annotation/AbstractValidation.php +++ b/src/Annotation/AbstractValidation.php @@ -2,11 +2,6 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule\Annotation; abstract class AbstractValidation diff --git a/src/Annotation/CsrfProtection.php b/src/Annotation/CsrfProtection.php index ba786fa..c40a0a4 100644 --- a/src/Annotation/CsrfProtection.php +++ b/src/Annotation/CsrfProtection.php @@ -2,11 +2,6 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule\Annotation; use Attribute; diff --git a/src/Annotation/FormValidation.php b/src/Annotation/FormValidation.php index 097f9ef..c852267 100644 --- a/src/Annotation/FormValidation.php +++ b/src/Annotation/FormValidation.php @@ -2,11 +2,6 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule\Annotation; use Attribute; @@ -16,7 +11,7 @@ final class FormValidation extends AbstractValidation { public function __construct( string $form = 'form', - public string|null $onFailure = null + public string|null $onFailure = null, ) { parent::__construct($form); } diff --git a/src/Annotation/InputValidation.php b/src/Annotation/InputValidation.php index d8dbb8f..77c1407 100644 --- a/src/Annotation/InputValidation.php +++ b/src/Annotation/InputValidation.php @@ -2,11 +2,6 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule\Annotation; use Attribute; diff --git a/src/Annotation/VndError.php b/src/Annotation/VndError.php index 29e89ba..34c5a56 100644 --- a/src/Annotation/VndError.php +++ b/src/Annotation/VndError.php @@ -2,11 +2,6 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule\Annotation; use Attribute; @@ -15,10 +10,7 @@ final class VndError { /** - * @param string $message * @param array $href - * @param string|null $logref - * @param string|null $path * * @see http://www.w3.org/TR/html5/links.html#link-type-help * @see http://tools.ietf.org/html/rfc6903#section-2 @@ -28,7 +20,7 @@ public function __construct( public string $message = '', public array $href = [], public string|null $logref = null, - public string|null $path = null + public string|null $path = null, ) { } } diff --git a/src/AntiCsrf.php b/src/AntiCsrf.php index 3d9a9b0..df4f16d 100644 --- a/src/AntiCsrf.php +++ b/src/AntiCsrf.php @@ -2,17 +2,14 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule; use Aura\Input\AntiCsrfInterface; use Aura\Input\Fieldset; use Aura\Session\Session; + use function is_bool; + use const PHP_SAPI; final class AntiCsrf implements AntiCsrfInterface @@ -23,22 +20,19 @@ final class AntiCsrf implements AntiCsrfInterface private bool $isCli; - private Session $session; - - public function __construct(Session $session, bool|null $isCli = null) + public function __construct(private Session $session, bool|null $isCli = null) { - $this->session = $session; $this->isCli = is_bool($isCli) ? $isCli : PHP_SAPI === 'cli'; } - public function setField(Fieldset $fieldset) : void + public function setField(Fieldset $fieldset): void { $fieldset->setField(self::TOKEN_KEY, 'hidden') ->setAttribs(['value' => $this->getToken()]); } /** @param array $data */ - public function isValid(array $data) : bool + public function isValid(array $data): bool { if ($this->isCli) { return true; @@ -47,7 +41,7 @@ public function isValid(array $data) : bool return isset($data[self::TOKEN_KEY]) && $data[self::TOKEN_KEY] === $this->getToken(); } - private function getToken() : string + private function getToken(): string { return $this->isCli ? self::TEST_TOKEN : $this->session->getCsrfToken()->getValue(); } diff --git a/src/AuraInputInterceptor.php b/src/AuraInputInterceptor.php index e2368af..676ae74 100644 --- a/src/AuraInputInterceptor.php +++ b/src/AuraInputInterceptor.php @@ -2,16 +2,9 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule; -use function array_shift; use Aura\Input\AntiCsrfInterface; -use function property_exists; use Ray\Aop\MethodInterceptor; use Ray\Aop\MethodInvocation; use Ray\Di\Di\Inject; @@ -23,13 +16,13 @@ use ReflectionClass; use ReflectionMethod; -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ +use function array_shift; +use function property_exists; + +/** @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AuraInputInterceptor implements MethodInterceptor { protected FailureHandlerInterface $failureHandler; - private AntiCsrfInterface|null $antiCsrf = null; public function __construct(FailureHandlerInterface $handler) @@ -38,13 +31,13 @@ public function __construct(FailureHandlerInterface $handler) } #[Inject] - public function setAntiCsrf(AntiCsrfInterface $antiCsrf) : void + public function setAntiCsrf(AntiCsrfInterface $antiCsrf): void { $this->antiCsrf = $antiCsrf; } /** - * {@inheritdoc} + * {@inheritDoc} * * @param MethodInvocation $invocation * @@ -76,15 +69,13 @@ public function invoke(MethodInvocation $invocation) * * @throws Exception\CsrfViolationException */ - public function isValid(array $submit, AbstractForm $form) : bool + public function isValid(array $submit, AbstractForm $form): bool { return $form->apply($submit); } - /** - * @throws InvalidArgumentException - */ - private function enableCsrfProtection(ReflectionMethod $method, AbstractForm $form) : void + /** @throws InvalidArgumentException */ + private function enableCsrfProtection(ReflectionMethod $method, AbstractForm $form): void { if ($method->getAttributes(CsrfProtection::class) === []) { return; @@ -97,7 +88,7 @@ private function enableCsrfProtection(ReflectionMethod $method, AbstractForm $fo $form->enableAntiCsrf($this->antiCsrf); } - private function getValidationAttribute(ReflectionMethod $method) : AbstractValidation|null + private function getValidationAttribute(ReflectionMethod $method): AbstractValidation|null { $attributes = $method->getAttributes(AbstractValidation::class, ReflectionAttribute::IS_INSTANCEOF); if ($attributes === []) { @@ -116,7 +107,7 @@ private function getValidationAttribute(ReflectionMethod $method) : AbstractVali * * @SuppressWarnings(PHPMD.Superglobals) */ - private function getNamedArguments(MethodInvocation $invocation) : array + private function getNamedArguments(MethodInvocation $invocation): array { $submit = []; $params = $invocation->getMethod()->getParameters(); @@ -133,7 +124,7 @@ private function getNamedArguments(MethodInvocation $invocation) : array return $submit; } - private function getFormProperty(AbstractValidation $formValidation, object $object) : AbstractForm + private function getFormProperty(AbstractValidation $formValidation, object $object): AbstractForm { if (! property_exists($object, $formValidation->form)) { throw new InvalidFormPropertyException($formValidation->form); diff --git a/src/AuraInputModule.php b/src/AuraInputModule.php index 803d9ea..dc09b81 100644 --- a/src/AuraInputModule.php +++ b/src/AuraInputModule.php @@ -2,11 +2,6 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule; use Aura\Filter\FilterFactory; @@ -22,15 +17,13 @@ use Ray\WebFormModule\Annotation\FormValidation; use Ray\WebFormModule\Annotation\InputValidation; -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ +/** @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AuraInputModule extends AbstractModule { - /** {@inheritdoc} */ + /** {@inheritDoc} */ protected function configure() { - $this->install(new AuraSessionModule); + $this->install(new AuraSessionModule()); $this->bind(BuilderInterface::class)->to(Builder::class); $this->bind(FilterInterface::class)->to(Filter::class); $this->bind(AntiCsrfInterface::class)->to(AntiCsrf::class)->in(Scope::SINGLETON); @@ -42,12 +35,12 @@ protected function configure() $this->bindInterceptor( $this->matcher->any(), $this->matcher->annotatedWith(InputValidation::class), - [InputValidationInterceptor::class] + [InputValidationInterceptor::class], ); $this->bindInterceptor( $this->matcher->any(), $this->matcher->annotatedWith(FormValidation::class), - [AuraInputInterceptor::class] + [AuraInputInterceptor::class], ); } } diff --git a/src/Exception/CsrfViolationException.php b/src/Exception/CsrfViolationException.php index 816cc28..3fe1bc2 100644 --- a/src/Exception/CsrfViolationException.php +++ b/src/Exception/CsrfViolationException.php @@ -2,11 +2,6 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule\Exception; use Aura\Input\Exception\CsrfViolation; diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php index dd435dc..495601e 100644 --- a/src/Exception/ExceptionInterface.php +++ b/src/Exception/ExceptionInterface.php @@ -2,11 +2,6 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule\Exception; interface ExceptionInterface diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php index 47323d4..0c1cfcf 100644 --- a/src/Exception/InvalidArgumentException.php +++ b/src/Exception/InvalidArgumentException.php @@ -2,11 +2,6 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule\Exception; class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface diff --git a/src/Exception/InvalidFormPropertyException.php b/src/Exception/InvalidFormPropertyException.php index fcb28e0..b11db09 100644 --- a/src/Exception/InvalidFormPropertyException.php +++ b/src/Exception/InvalidFormPropertyException.php @@ -2,11 +2,6 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule\Exception; class InvalidFormPropertyException extends LogicException diff --git a/src/Exception/InvalidOnFailureMethod.php b/src/Exception/InvalidOnFailureMethod.php index 1e0c20e..002b52a 100644 --- a/src/Exception/InvalidOnFailureMethod.php +++ b/src/Exception/InvalidOnFailureMethod.php @@ -2,11 +2,6 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule\Exception; class InvalidOnFailureMethod extends LogicException diff --git a/src/Exception/LogicException.php b/src/Exception/LogicException.php index da9cee0..4a50d27 100644 --- a/src/Exception/LogicException.php +++ b/src/Exception/LogicException.php @@ -2,11 +2,6 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule\Exception; class LogicException extends \LogicException implements ExceptionInterface diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php index 345d956..5ec6384 100644 --- a/src/Exception/RuntimeException.php +++ b/src/Exception/RuntimeException.php @@ -2,11 +2,6 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule\Exception; class RuntimeException extends \RuntimeException implements ExceptionInterface diff --git a/src/Exception/ValidationException.php b/src/Exception/ValidationException.php index aa557b0..86b6a11 100644 --- a/src/Exception/ValidationException.php +++ b/src/Exception/ValidationException.php @@ -2,23 +2,16 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule\Exception; use Exception; use Ray\WebFormModule\FormValidationError; +use Throwable; class ValidationException extends Exception { - public ?FormValidationError $error; - - public function __construct(string $message = '', int $code = 0, ?Exception $e = null, ?FormValidationError $error = null) + public function __construct(string $message = '', int $code = 0, Throwable|null $e = null, public FormValidationError|null $error = null) { parent::__construct($message, $code, $e); - $this->error = $error; } } diff --git a/src/FailureHandlerInterface.php b/src/FailureHandlerInterface.php index 67c0eb0..ce0d3f4 100644 --- a/src/FailureHandlerInterface.php +++ b/src/FailureHandlerInterface.php @@ -2,11 +2,6 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule; use Ray\Aop\MethodInvocation; @@ -15,9 +10,7 @@ interface FailureHandlerInterface { /** - * @param AbstractValidation $formValidation * @param MethodInvocation $invocation - * @param AbstractForm $form * * @return mixed */ diff --git a/src/FormFactory.php b/src/FormFactory.php index ae72d72..5fcd813 100644 --- a/src/FormFactory.php +++ b/src/FormFactory.php @@ -2,11 +2,6 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule; use Aura\Filter\FilterFactory; @@ -16,13 +11,10 @@ final class FormFactory { /** - * @param string $class - * - * @phpstan-param class-string $class - * * @psalm-param class-string $class + * @phpstan-param class-string $class */ - public function newInstance(string $class) : AbstractForm + public function newInstance(string $class): AbstractForm { $form = new $class(); $form->setBaseDependencies(new Builder(), new FilterFactory(), new HelperLocatorFactory()); diff --git a/src/FormInterface.php b/src/FormInterface.php index ab0e9e0..0caf3e8 100644 --- a/src/FormInterface.php +++ b/src/FormInterface.php @@ -2,24 +2,21 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule; +use Aura\Input\Exception\NoSuchInput; + interface FormInterface { /** * Return input element html * - * @throws \Aura\Input\Exception\NoSuchInput - * * @return string + * + * @throws NoSuchInput */ public function input(string $input); /** Return error message */ - public function error(string $input) : string; + public function error(string $input): string; } diff --git a/src/FormValidationError.php b/src/FormValidationError.php index 75b95b8..77929e3 100644 --- a/src/FormValidationError.php +++ b/src/FormValidationError.php @@ -2,31 +2,21 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule; use function json_encode; + use const JSON_PRETTY_PRINT; use const JSON_UNESCAPED_SLASHES; class FormValidationError { - /** @var array */ - private array $value; - - /** - * @param array $value - */ - public function __construct(array $value) + /** @param array $value */ + public function __construct(private array $value) { - $this->value = $value; } - public function __toString() : string + public function __toString(): string { return (string) json_encode($this->value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); } diff --git a/src/FormVndErrorModule.php b/src/FormVndErrorModule.php index b8e76f4..2db7c3b 100644 --- a/src/FormVndErrorModule.php +++ b/src/FormVndErrorModule.php @@ -2,18 +2,13 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule; use Ray\Di\AbstractModule; class FormVndErrorModule extends AbstractModule { - /** {@inheritdoc} */ + /** {@inheritDoc} */ protected function configure() { $this->bind(FailureHandlerInterface::class)->to(VndErrorHandler::class); diff --git a/src/InputValidationInterceptor.php b/src/InputValidationInterceptor.php index fec9ef9..81f2261 100644 --- a/src/InputValidationInterceptor.php +++ b/src/InputValidationInterceptor.php @@ -2,11 +2,6 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule; use Ray\Di\Di\Named; diff --git a/src/OnFailureMethodHandler.php b/src/OnFailureMethodHandler.php index 0a10936..e7aa796 100644 --- a/src/OnFailureMethodHandler.php +++ b/src/OnFailureMethodHandler.php @@ -2,30 +2,25 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule; -use function call_user_func_array; -use function method_exists; use Ray\Aop\MethodInvocation; use Ray\WebFormModule\Annotation\AbstractValidation; use Ray\WebFormModule\Annotation\FormValidation; use Ray\WebFormModule\Exception\InvalidOnFailureMethod; +use function call_user_func_array; +use function get_class; +use function method_exists; + final class OnFailureMethodHandler implements FailureHandlerInterface { public const FAILURE_SUFFIX = 'ValidationFailed'; /** - * {@inheritdoc} + * {@inheritDoc} * - * @param AbstractValidation $formValidation * @param MethodInvocation $invocation - * @param AbstractForm $form */ public function handle(AbstractValidation $formValidation, MethodInvocation $invocation, AbstractForm $form) { diff --git a/src/SetAntiCsrfTrait.php b/src/SetAntiCsrfTrait.php index d746279..cee30ae 100644 --- a/src/SetAntiCsrfTrait.php +++ b/src/SetAntiCsrfTrait.php @@ -2,11 +2,6 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule; use Aura\Input\AntiCsrfInterface; @@ -15,7 +10,7 @@ trait SetAntiCsrfTrait { #[Inject] - public function setAntiCsrf(AntiCsrfInterface $antiCsrf) : void + public function setAntiCsrf(AntiCsrfInterface $antiCsrf): void { $this->antiCsrf = $antiCsrf; } diff --git a/src/SubmitInterface.php b/src/SubmitInterface.php index d902e64..c9360f7 100644 --- a/src/SubmitInterface.php +++ b/src/SubmitInterface.php @@ -2,11 +2,6 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule; interface SubmitInterface diff --git a/src/ToStringInterface.php b/src/ToStringInterface.php index 3ce4a09..852af69 100644 --- a/src/ToStringInterface.php +++ b/src/ToStringInterface.php @@ -2,15 +2,10 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule; /** Return form markup string */ interface ToStringInterface { - public function toString() : string; + public function toString(): string; } diff --git a/src/VndErrorHandler.php b/src/VndErrorHandler.php index a334af4..23a0555 100644 --- a/src/VndErrorHandler.php +++ b/src/VndErrorHandler.php @@ -2,11 +2,6 @@ declare(strict_types=1); -/** - * This file is part of the Ray.WebFormModule package. - * - * @license http://opensource.org/licenses/MIT MIT - */ namespace Ray\WebFormModule; use Ray\Aop\MethodInvocation; @@ -18,11 +13,9 @@ final class VndErrorHandler implements FailureHandlerInterface { /** - * {@inheritdoc} + * {@inheritDoc} * - * @param AbstractValidation $formValidation * @param MethodInvocation $invocation - * @param AbstractForm $form */ public function handle(AbstractValidation $formValidation, MethodInvocation $invocation, AbstractForm $form) { @@ -33,7 +26,7 @@ public function handle(AbstractValidation $formValidation, MethodInvocation $inv throw new ValidationException('Validation failed.', 400, null, $error); } - private function getVndErrorAttribute(ReflectionMethod $method) : VndError|null + private function getVndErrorAttribute(ReflectionMethod $method): VndError|null { $attributes = $method->getAttributes(VndError::class); if ($attributes === []) { @@ -48,7 +41,7 @@ private function getVndErrorAttribute(ReflectionMethod $method) : VndError|null * * @SuppressWarnings(PHPMD.Superglobals) */ - private function makeVndError(AbstractForm $form, ?VndError $vndError = null) : array + private function makeVndError(AbstractForm $form, VndError|null $vndError = null): array { $body = ['message' => 'Validation failed']; $body['path'] = $_SERVER['PATH_INFO'] ?? ''; @@ -57,10 +50,8 @@ private function makeVndError(AbstractForm $form, ?VndError $vndError = null) : return $vndError ? $this->optionalAttribute($vndError) + $body : $body; } - /** - * @return array - */ - private function optionalAttribute(VndError $vndError) : array + /** @return array */ + private function optionalAttribute(VndError $vndError): array { $body = []; if ($vndError->message) { diff --git a/tests/AbstractAuraFormTest.php b/tests/AbstractAuraFormTest.php index 10eba46..d33e6e8 100644 --- a/tests/AbstractAuraFormTest.php +++ b/tests/AbstractAuraFormTest.php @@ -1,25 +1,26 @@ form = (new FormFactory)->newInstance(FakeForm::class); + + $this->form = (new FormFactory())->newInstance(FakeForm::class); } public function testForm() @@ -30,7 +31,7 @@ public function testForm() public function testAntiCsrfForm() { - $this->form->setAntiCsrf(new FakeAntiCsrf); + $this->form->setAntiCsrf(new FakeAntiCsrf()); $this->form->postConstruct(); $formHtml = $this->form->form(); $this->assertSame('
' . PHP_EOL, $formHtml); @@ -42,7 +43,7 @@ public function testInput() $this->assertSame('' . PHP_EOL, (string) $name); } - public function testError() + public function testError(): string { $this->form->fill([]); $data = ['name' => '@invalid@']; @@ -50,15 +51,12 @@ public function testError() $this->assertFalse($isValid); $error = $this->form->error('name'); $this->assertSame('Name must be alphabetic only.', $error); - $html = (string) $this->form; - return $html; + return (string) $this->form; } - /** - * @depends testError - */ - public function tesetInputDataReamainedOnValidationFailure($html) + /** @depends testError */ + public function tesetInputDataReamainedOnValidationFailure(string $html): void { $expected = ''; $this->assertContains($expected, $html); @@ -67,11 +65,11 @@ public function tesetInputDataReamainedOnValidationFailure($html) public function testNotToStringImplemented() { $errNo = $errStr = ''; - set_error_handler(function (int $no, string $str) use (&$errNo, &$errStr) { + set_error_handler(static function (int $no, string $str) use (&$errNo, &$errStr) { $errNo = $no; $errStr = $str; }); - $form = new FakeErrorForm; + $form = new FakeErrorForm(); (string) $form; $this->assertSame(256, $errNo); restore_error_handler(); diff --git a/tests/AbstractFormTest.php b/tests/AbstractFormTest.php index 2e8c9b3..c306eb2 100644 --- a/tests/AbstractFormTest.php +++ b/tests/AbstractFormTest.php @@ -1,10 +1,7 @@ form = (new FormFactory)->newInstance(FakeMiniForm::class); + + $this->form = (new FormFactory())->newInstance(FakeMiniForm::class); } - /** - * @param $method - */ - public function getMethodInvocation(array $arguments) + /** @param array $arguments */ + public function getMethodInvocation(array $arguments): ReflectiveMethodInvocation { // form - $fakeForm = (new FormFactory)->newInstance(FakeMiniForm::class); + $fakeForm = (new FormFactory())->newInstance(FakeMiniForm::class); // controller - $controller = new FakeController; + $controller = new FakeController(); $controller->setForm($fakeForm); // interceptor $interceptor = new AuraInputInterceptor(new VndErrorHandler()); @@ -47,9 +45,7 @@ public function getMethodInvocation(array $arguments) $controller, 'createAction', $arguments, - [ - $interceptor - ] + [$interceptor], ); } @@ -77,25 +73,25 @@ public function testErrorReturnEmpty() public function testClone() { $form = clone $this->form; - (new \ReflectionProperty($form, 'filter'))->setAccessible(true); - (new \ReflectionProperty($this->form, 'filter'))->setAccessible(true); + (new ReflectionProperty($form, 'filter'))->setAccessible(true); + (new ReflectionProperty($this->form, 'filter'))->setAccessible(true); $this->assertNotSame(spl_object_hash($form), spl_object_hash($this->form)); } public function testGetItelator() { $itelator = $this->form->getIterator(); - $this->assertInstanceOf(\Iterator::class, $itelator); + $this->assertInstanceOf(Iterator::class, $itelator); } public function testAntiCsrfViolation() { $this->expectException(CsrfViolationException::class); $session = new Session( - new SegmentFactory, - new CsrfTokenFactory(new Randval(new Phpfunc)), - new FakePhpfunc, - [] + new SegmentFactory(), + new CsrfTokenFactory(new Randval(new Phpfunc())), + new FakePhpfunc(), + [], ); $this->form->setAntiCsrf(new AntiCsrf($session, false)); $this->form->apply([]); diff --git a/tests/AntiCsrfTest.php b/tests/AntiCsrfTest.php index 6b4a6a8..708c81d 100644 --- a/tests/AntiCsrfTest.php +++ b/tests/AntiCsrfTest.php @@ -1,10 +1,7 @@ phpfunc = new FakePhpfunc; + $this->phpfunc = new FakePhpfunc(); $this->session = $this->newSession(); $this->antiCsrf = new AntiCsrf($this->newSession([]), false, [AntiCsrf::TOKEN_KEY => AntiCsrf::TEST_TOKEN]); } @@ -45,7 +34,7 @@ public function testNew() public function testSetField() { - $result = $this->antiCsrf->setField(new Fieldset(new Builder, new Filter)); + $result = $this->antiCsrf->setField(new Fieldset(new Builder(), new Filter())); $this->assertNull($result); } @@ -55,13 +44,14 @@ public function testIsValid() $this->assertTrue($this->antiCsrf->isValid($data)); } - protected function newSession(array $cookies = []) + /** @param array $cookies */ + protected function newSession(array $cookies = []): Session { return new Session( - new SegmentFactory, + new SegmentFactory(), new CsrfTokenFactory(new Randval(new Phpfunc())), $this->phpfunc, - $cookies + $cookies, ); } } diff --git a/tests/AuraInputInterceptorTest.php b/tests/AuraInputInterceptorTest.php index 0b2743e..ff7b065 100644 --- a/tests/AuraInputInterceptorTest.php +++ b/tests/AuraInputInterceptorTest.php @@ -1,10 +1,7 @@ injector = new Injector(new class() extends AbstractModule { + $this->injector = new Injector(new class () extends AbstractModule { protected function configure() { - $this->install(new AuraInputModule); + $this->install(new AuraInputModule()); $this->bind(FormInterface::class)->annotatedWith('contact_form')->to(FakeForm::class); $this->bind(FormInterface::class)->annotatedWith('mini_form')->to(FakeMiniForm::class); } diff --git a/tests/AuraInputModuleTest.php b/tests/AuraInputModuleTest.php index e1d3cfb..d08deed 100644 --- a/tests/AuraInputModuleTest.php +++ b/tests/AuraInputModuleTest.php @@ -1,10 +1,7 @@ getInstance(FakeController::class); $this->assertInstanceOf(WeavedInterface::class, $controller); } @@ -24,8 +21,8 @@ public function testAuraInputModule() public function testExceptionOnFailure() { $this->expectException(ValidationException::class); - $injector = new Injector(new FakeModule, __DIR__ . '/tmp'); - /** @var $controller FakeInputValidationController */ + $injector = new Injector(new FakeModule(), __DIR__ . '/tmp'); + /** @var FakeInputValidationController $controller */ $controller = $injector->getInstance(FakeInputValidationController::class); $controller->createAction(''); } diff --git a/tests/FormFactoryTest.php b/tests/FormFactoryTest.php index e3ecef9..59d6f2a 100644 --- a/tests/FormFactoryTest.php +++ b/tests/FormFactoryTest.php @@ -1,25 +1,21 @@ factory = new FormFactory; + + $this->factory = new FormFactory(); } public function testNewInstance() diff --git a/tests/VndErrorHandlerTest.php b/tests/VndErrorHandlerTest.php index 6f350b4..4db8b3b 100644 --- a/tests/VndErrorHandlerTest.php +++ b/tests/VndErrorHandlerTest.php @@ -1,10 +1,7 @@ controller = (new Injector(new FakeVndErrorModule, __DIR__ . '/tmp'))->getInstance(FakeController::class); + + $this->controller = (new Injector(new FakeVndErrorModule(), __DIR__ . '/tmp'))->getInstance(FakeController::class); } public function testValidationException() @@ -50,8 +46,8 @@ public function testValidationExceptionError() public function testVndErrorAnnotation() { - /** @var $controller FakeControllerVndError */ - $controller = (new Injector(new FakeVndErrorModule))->getInstance(FakeControllerVndError::class); + /** @var FakeControllerVndError $controller */ + $controller = (new Injector(new FakeVndErrorModule()))->getInstance(FakeControllerVndError::class); try { $controller->createAction(''); } catch (ValidationException $e) { diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 039e44c..ab50824 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,18 +1,17 @@ Date: Sun, 17 May 2026 11:31:01 +0900 Subject: [PATCH 3/8] docs: document #[CsrfProtection] opt-in and add 0.x migration guide - CHANGELOG: mark #[CsrfProtection] as BC break with antiCsrf=true Before/After example - CHANGELOG: add BC break entry for ValidationException constructor type strengthening - README (EN/JA): explain CSRF protection is opt-in (no check without #[CsrfProtection]) - README (EN/JA): add "Migration from 0.x" mapping table --- CHANGELOG.md | 29 ++++++++++++++++++++++++++--- README.JA.md | 20 +++++++++++++++++++- README.md | 21 +++++++++++++++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9625f4..dde77f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,9 +13,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BC break**: Migrated from Doctrine Annotations to PHP 8 Attributes. All validation metadata (`@FormValidation`, `@InputValidation`, `@VndError`) is now expressed with `#[FormValidation]`, `#[InputValidation]`, `#[VndError]`. -- CSRF protection for validation methods is declared with the separate - `#[CsrfProtection]` attribute instead of a boolean option on - `#[FormValidation]`. +- **BC break**: CSRF protection for validation methods is now declared with + the separate `#[CsrfProtection]` attribute. The previous `antiCsrf=true` + boolean option on `@FormValidation` has been removed. CSRF checks are now + opt-in: methods without `#[CsrfProtection]` perform no CSRF verification + even if the form has an `AntiCsrf` object set. + + Before: + + ```php + /** + * @FormValidation(form="contactForm", antiCsrf=true) + */ + public function createAction() {} + ``` + + After: + + ```php + #[FormValidation(form: 'contactForm')] + #[CsrfProtection] + public function createAction() {} + ``` - **BC break**: `AuraInputInterceptor`, `InputValidationInterceptor` and `VndErrorHandler` no longer accept a `Doctrine\Common\Annotations\Reader` in their constructors. Validation attributes are read directly via @@ -23,6 +42,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BC break**: `FormInterface::input()` and `FormInterface::error()` now declare parameter and return types (`string $input`, `: string` respectively). Implementations must update their signatures. +- **BC break**: `ValidationException::__construct()` now declares parameter + types (`string $message`, `int $code`, `Throwable|null $e`, + `FormValidationError|null $error`). The `$error` property is now typed as + `FormValidationError|null` via constructor property promotion. - Added property type declarations and return types across the codebase to align with PHP 8 typing. - Bumped dependencies: `ray/di` `^2.16`, `ray/aop` `^2.14`, diff --git a/README.JA.md b/README.JA.md index 4dac375..366fb1d 100644 --- a/README.JA.md +++ b/README.JA.md @@ -124,7 +124,9 @@ class MyController ### CSRF Protections -CSRF対策を行うためにはフォームにCSRFオブジェクトをセットします。 +CSRF対策は **opt-in** です。`SetAntiCsrfTrait` を使うフォームには `AntiCsrfInterface` が注入されますが、 +トークンの検証は `#[CsrfProtection]` 属性が付いたメソッドでのみ行われます。 +`#[CsrfProtection]` が無いメソッドでは、フォーム側に AntiCsrf がセットされていても CSRF チェックは実行されません。 ```php use Ray\WebFormModule\AbstractAuraForm; @@ -150,6 +152,22 @@ class MyController セキュリティレベルを高めるためにはユーザーの認証を含んだカスタムCsrfクラスを作成してフォームクラスにセットします。 詳しくはAura.Inputの[Applying CSRF Protections](https://github.com/auraphp/Aura.Input#applying-csrf-protections)をご覧ください。 +## 0.x からのマイグレーション + +1.0 で Doctrine Annotations を廃止し、PHP 8 Attributes に完全移行しました。 +型宣言も強化されています。主な書き換え: + +| Before (0.x) | After (1.0) | +|--------------------------------------------------------------------|---------------------------------------------------------------------------| +| `@FormValidation(form="f", onFailure="badRequest")` | `#[FormValidation(form: 'f', onFailure: 'badRequest')]` | +| `@FormValidation(form="f", antiCsrf=true)` | `#[FormValidation(form: 'f')]` + `#[CsrfProtection]` | +| `@InputValidation(form="f")` | `#[InputValidation(form: 'f')]` | +| `@VndError(message="...", logref="...")` | `#[VndError(message: '...', logref: '...')]` | +| `new AuraInputInterceptor($injector, $reader)` | `new AuraInputInterceptor($injector)` (`Reader` 引数は不要) | +| `public function input($input)` / `public function error($input)` | `public function input(string $input): string` / `error(string $input): string` | + +破壊的変更の完全なリストは [CHANGELOG.md](CHANGELOG.md) を参照してください。 + ## Validation Exception `#[FormValidation]`の代わりに`#[InputValidation]`とアノテートするとバリデーションが失敗したときに`Ray\WebFormModule\Exception\ValidationException`が投げられるよになります。この場合はHTML表現は使われません。Web APIアプリケーションなどに便利です。 diff --git a/README.md b/README.md index 933efe4..2d93640 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,11 @@ or render input element basis. ``` ## CSRF Protections +CSRF protection is **opt-in**. A form that uses `SetAntiCsrfTrait` is wired +with an `AntiCsrfInterface`, but the token is only verified when the +validated method is annotated with `#[CsrfProtection]`. Methods without +`#[CsrfProtection]` perform no CSRF check even if the form supports it. + ```php use Ray\WebFormModule\AbstractAuraForm; use Ray\WebFormModule\Annotation\CsrfProtection; @@ -160,6 +165,22 @@ class MyController ``` You can provide your custom `AntiCsrf` class. See more detail at [Aura.Input](https://github.com/auraphp/Aura.Input#applying-csrf-protections) +## Migration from 0.x + +Version 1.0 drops Doctrine Annotations in favour of native PHP 8 Attributes +and tightens type declarations. The most common rewrites: + +| Before (0.x) | After (1.0) | +|--------------------------------------------------------------------|---------------------------------------------------------------------------| +| `@FormValidation(form="f", onFailure="badRequest")` | `#[FormValidation(form: 'f', onFailure: 'badRequest')]` | +| `@FormValidation(form="f", antiCsrf=true)` | `#[FormValidation(form: 'f')]` + `#[CsrfProtection]` | +| `@InputValidation(form="f")` | `#[InputValidation(form: 'f')]` | +| `@VndError(message="...", logref="...")` | `#[VndError(message: '...', logref: '...')]` | +| `new AuraInputInterceptor($injector, $reader)` | `new AuraInputInterceptor($injector)` (no `Reader` argument) | +| `public function input($input)` / `public function error($input)` | `public function input(string $input): string` / `error(string $input): string` | + +See [CHANGELOG.md](CHANGELOG.md) for the full list of breaking changes. + ## Validation Exception When we install `Ray\WebFormModule\FormVndErrorModule` as following, From 807e30ae7e31d59b505f0fc9f93141de270cf504 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Sun, 17 May 2026 11:35:06 +0900 Subject: [PATCH 4/8] docs: add Claude Code skill for 0.x to 1.0 migration - .claude/skills/migrate-to-1.0/SKILL.md: step-by-step migration skill covering annotation -> attribute, antiCsrf=true split into #[CsrfProtection], Reader argument removal, and FormInterface signature updates - README (EN/JA): link the skill from the Migration from 0.x section --- .claude/skills/migrate-to-1.0/SKILL.md | 200 +++++++++++++++++++++++++ README.JA.md | 9 ++ README.md | 10 ++ 3 files changed, 219 insertions(+) create mode 100644 .claude/skills/migrate-to-1.0/SKILL.md diff --git a/.claude/skills/migrate-to-1.0/SKILL.md b/.claude/skills/migrate-to-1.0/SKILL.md new file mode 100644 index 0000000..c8cc95e --- /dev/null +++ b/.claude/skills/migrate-to-1.0/SKILL.md @@ -0,0 +1,200 @@ +--- +name: migrate-to-1.0 +description: Migrate a project consuming ray/web-form-module from 0.x (Doctrine Annotations) to 1.0 (PHP 8 Attributes). Detects @FormValidation / @InputValidation / @VndError annotations, antiCsrf=true options, AuraInputInterceptor Reader arguments, and FormInterface signature mismatches; rewrites them and reports remaining manual steps. +--- + +# Migrate ray/web-form-module 0.x → 1.0 + +Apply this skill in a project that depends on `ray/web-form-module` and is +upgrading from `0.x` to `1.0`. Run it from the project root. + +## Scope + +You will rewrite consumer code only. Do NOT touch files under +`vendor/`. Limit edits to: + +- `src/`, `app/`, `tests/`, `lib/` and similar source roots +- PHP files only (`*.php`) + +Skip generated/cache directories: `var/`, `cache/`, `tmp/`, `build/`, +`node_modules/`, `.git/`. + +## Preflight + +Before changing any file: + +1. Confirm the project's `composer.json` declares `ray/web-form-module`. + If not, abort and tell the user this skill does not apply. +2. Confirm the working tree is clean (`git status`). If not, ask the user + whether to proceed — uncommitted changes will be mixed with this skill's + edits. +3. Bump the constraint to `^1.0` in `composer.json` (`require` section). If + `doctrine/annotations` is in `require` and is no longer used elsewhere, + remove it. + +## Step 1 — Rewrite validation annotations to attributes + +For each PHP file under the source roots: + +### 1a. `@FormValidation` + +Find docblock annotations of the form: + +```php +/** + * @FormValidation(form="contactForm", onFailure="badRequestAction") + */ +public function createAction() +``` + +Rewrite to a native attribute on the method: + +```php +#[FormValidation(form: 'contactForm', onFailure: 'badRequestAction')] +public function createAction() +``` + +Rules: + +- Remove the annotation line from the docblock. If the docblock becomes + empty (only `/**` and `*/`), delete the entire docblock. +- Convert `=` to `:` and double-quoted strings to single-quoted PHP strings. +- Preserve the existing `use Ray\WebFormModule\Annotation\FormValidation;` + import. Add it if missing. + +### 1b. `@FormValidation(... antiCsrf=true)` — **BC break** + +The `antiCsrf` option was removed. Split it into two attributes: + +```php +/** + * @FormValidation(form="contactForm", antiCsrf=true) + */ +``` + +becomes + +```php +#[FormValidation(form: 'contactForm')] +#[CsrfProtection] +``` + +Also add `use Ray\WebFormModule\Annotation\CsrfProtection;`. + +If `antiCsrf=false` (or omitted), drop the option without adding +`#[CsrfProtection]` — methods without the attribute perform no CSRF check. + +### 1c. `@InputValidation` and `@VndError` + +Same treatment as `@FormValidation` (no `antiCsrf` concern). + +```php +/** @InputValidation(form="form1") */ +public function createAction($name) {} +``` + +```php +/** + * @VndError(message="...", logref="a1000", path="/p", href={"_self"="/p"}) + */ +``` + +become + +```php +#[InputValidation(form: 'form1')] +public function createAction($name) {} +``` + +```php +#[VndError(message: '...', logref: 'a1000', path: '/p', href: ['_self' => '/p'])] +``` + +Note the `{...}` (Doctrine array literal) → `[...]` (PHP array) conversion +in `VndError`'s `href`. + +## Step 2 — Drop `Reader` arguments + +`AuraInputInterceptor`, `InputValidationInterceptor`, and `VndErrorHandler` +no longer accept a `Doctrine\Common\Annotations\Reader`. + +Find direct `new` calls and DI module bindings such as: + +```php +new AuraInputInterceptor($injector, $reader) +new InputValidationInterceptor($injector, $reader, $handler) +new VndErrorHandler($reader) +``` + +Drop the `Reader` argument: + +```php +new AuraInputInterceptor($injector) +new InputValidationInterceptor($injector, $handler) +new VndErrorHandler() +``` + +If the project bound `Doctrine\Common\Annotations\Reader` in a Ray.Di +module solely to satisfy these constructors, remove that binding. + +## Step 3 — Update `FormInterface` implementations + +`FormInterface::input()` and `FormInterface::error()` now declare types: + +```php +public function input(string $input): string; +public function error(string $input): string; +``` + +Any project class implementing `FormInterface` (directly, or via +`AbstractForm` override) must match these signatures. Update: + +```php +public function input($input) +public function error($input) +``` + +to: + +```php +public function input(string $input): string +public function error(string $input): string +``` + +## Step 4 — Update `ValidationException` callers + +`ValidationException::__construct(string $message = '', int $code = 0, +?Throwable $e = null, ?FormValidationError $error = null)` — the third +parameter is now `Throwable|null` (was `Exception|null`) and `$error` is +typed `FormValidationError|null`. + +If the project passes a non-`Throwable` value, fix the call site. + +## Step 5 — Verify + +After all rewrites: + +1. Run the project's static analysis (`composer phpstan`, `composer psalm`, + `composer cs` — whichever exist). +2. Run the project's test suite. +3. `grep -rn "@FormValidation\|@InputValidation\|@VndError\|antiCsrf" src/ tests/ app/ 2>/dev/null` + should return nothing relevant. +4. `grep -rn "Doctrine\\\\Common\\\\Annotations\\\\Reader" src/ app/ 2>/dev/null` + should not show usages tied to this package. + +Report to the user: + +- Files modified (count + paths). +- Any annotation occurrence you could not rewrite automatically (e.g., + uncommon formatting). List file:line so the user can finish manually. +- Static analysis / test results. + +## Out of scope + +- Migrating other libraries' annotations (only `Ray\WebFormModule\Annotation\*`). +- Renaming `SetAntiCsrfTrait` — the trait and `setAntiCsrf()` method are + unchanged in 1.0. +- Changes to forms that use `AntiCsrf` without `#[CsrfProtection]` — + flag these to the user: in 1.0 they will silently stop enforcing CSRF. + The user must decide whether to add `#[CsrfProtection]` to the validated + controller method or accept the new behaviour. diff --git a/README.JA.md b/README.JA.md index 366fb1d..c8d4bd6 100644 --- a/README.JA.md +++ b/README.JA.md @@ -168,6 +168,15 @@ class MyController 破壊的変更の完全なリストは [CHANGELOG.md](CHANGELOG.md) を参照してください。 +### Claude Code による自動マイグレーション + +リポジトリ同梱の Claude Code skill +[`.claude/skills/migrate-to-1.0/SKILL.md`](.claude/skills/migrate-to-1.0/SKILL.md) +が上記の書き換え (アノテーション → アトリビュート、`antiCsrf=true` の +`#[CsrfProtection]` 分割、`Reader` 引数削除、`FormInterface` 署名更新) を +AI アシスタントに案内します。利用側プロジェクトの `.claude/skills/` に +ディレクトリをコピーして `/migrate-to-1.0` で起動してください。 + ## Validation Exception `#[FormValidation]`の代わりに`#[InputValidation]`とアノテートするとバリデーションが失敗したときに`Ray\WebFormModule\Exception\ValidationException`が投げられるよになります。この場合はHTML表現は使われません。Web APIアプリケーションなどに便利です。 diff --git a/README.md b/README.md index 2d93640..06d94bb 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,16 @@ and tightens type declarations. The most common rewrites: See [CHANGELOG.md](CHANGELOG.md) for the full list of breaking changes. +### Automated migration with Claude Code + +The repository ships a Claude Code skill at +[`.claude/skills/migrate-to-1.0/SKILL.md`](.claude/skills/migrate-to-1.0/SKILL.md) +that walks an AI assistant through the rewrites above (annotations → +attributes, `antiCsrf=true` split into `#[CsrfProtection]`, `Reader` +argument removal, `FormInterface` signature updates). Copy the directory +into your consuming project's `.claude/skills/` and invoke it via +`/migrate-to-1.0`. + ## Validation Exception When we install `Ray\WebFormModule\FormVndErrorModule` as following, From 643a8c053c3dea2eb776bd8f861009840c62b1af Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Sun, 17 May 2026 11:44:33 +0900 Subject: [PATCH 5/8] fix: address CodeRabbit review feedback - tests/AbstractAuraFormTest.php: fix typo so PHPUnit discovers the test (tesetInputDataReamainedOnValidationFailure -> testInputDataRemainedOnValidationFailure) and switch deprecated assertContains to assertStringContainsString - src/AbstractForm.php: guard null in getFailureMessages() (filter may have no failures yet); previously dereferenced a possibly-null return - .github/workflows/static-analysis.yml: fall back to PHP 8.4 when inputs.php_version is unavailable (push/pull_request events) --- .github/workflows/static-analysis.yml | 8 ++++---- src/AbstractForm.php | 7 ++++++- tests/AbstractAuraFormTest.php | 4 ++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 4af412a..1c3a063 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -25,7 +25,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ inputs.php_version }} + php-version: ${{ inputs.php_version || '8.4' }} tools: cs2pr coverage: none @@ -58,7 +58,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ inputs.php_version }} + php-version: ${{ inputs.php_version || '8.4' }} tools: cs2pr coverage: none @@ -84,7 +84,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ inputs.php_version }} + php-version: ${{ inputs.php_version || '8.4' }} - name: Get composer cache directory id: composer-cache @@ -115,7 +115,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ inputs.php_version }} + php-version: ${{ inputs.php_version || '8.4' }} coverage: none - name: Get composer cache directory diff --git a/src/AbstractForm.php b/src/AbstractForm.php index b5a68f7..c58aa8f 100644 --- a/src/AbstractForm.php +++ b/src/AbstractForm.php @@ -181,8 +181,13 @@ public function apply(array $data): bool */ public function getFailureMessages(): array { + $failure = $this->filter->getFailures(); + if ($failure === null) { + return []; + } + /** @var array> $messages */ - $messages = $this->filter->getFailures()->getMessages(); + $messages = $failure->getMessages(); return $messages; } diff --git a/tests/AbstractAuraFormTest.php b/tests/AbstractAuraFormTest.php index d33e6e8..5f48111 100644 --- a/tests/AbstractAuraFormTest.php +++ b/tests/AbstractAuraFormTest.php @@ -56,10 +56,10 @@ public function testError(): string } /** @depends testError */ - public function tesetInputDataReamainedOnValidationFailure(string $html): void + public function testInputDataRemainedOnValidationFailure(string $html): void { $expected = ''; - $this->assertContains($expected, $html); + $this->assertStringContainsString($expected, $html); } public function testNotToStringImplemented() From a2fa31939f14d1bb69bf1cddb36f8b6bed7c6aef Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Sun, 17 May 2026 11:47:45 +0900 Subject: [PATCH 6/8] docs: align README.md heading levels with README.JA.md - "CSRF Protections" demoted from ## to ### (it is a Usage subsection) - "Demo" promoted from ### to ## (standalone section) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 06d94bb..b880d99 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ or render input element basis. echo $form->input('name'); // echo $form->error('name'); // "Name must be alphabetic only." or blank. ``` -## CSRF Protections +### CSRF Protections CSRF protection is **opt-in**. A form that uses `SetAntiCsrfTrait` is wired with an `AntiCsrfInterface`, but the token is only verified when the @@ -232,6 +232,6 @@ More detail for `vnd.error+json` can be added with the `#[VndError]` attribute. This optional module is handy for API application. -### Demo +## Demo $ php -S docs/demo/1.csrf/web.php From 684b1320a47611885c17dadb75938c2286c5231c Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Sun, 17 May 2026 11:48:56 +0900 Subject: [PATCH 7/8] ci: add PHP 8.5 to the test matrix --- .github/workflows/continuous-integration.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 47ee88f..71f9c1f 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -24,6 +24,7 @@ jobs: - '8.2' - '8.3' - '8.4' + - '8.5' steps: - name: Checkout uses: actions/checkout@v4 From e0c6691d957280af64a191ea3874adbba8c67d11 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Sun, 17 May 2026 11:52:15 +0900 Subject: [PATCH 8/8] fix: silence PHPStan identical.alwaysFalse in getFailureMessages The Aura\Filter\SubjectFilter::getFailures() PHPDoc declares a non-null return, but the runtime value can be null before apply() runs. Annotate the local variable as FailureCollection|null so the null guard added in 643a8c0 type-checks (mirrors the pattern already used in error()). --- src/AbstractForm.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/AbstractForm.php b/src/AbstractForm.php index c58aa8f..4a0431f 100644 --- a/src/AbstractForm.php +++ b/src/AbstractForm.php @@ -181,6 +181,7 @@ public function apply(array $data): bool */ public function getFailureMessages(): array { + /** @var FailureCollection|null $failure */ $failure = $this->filter->getFailures(); if ($failure === null) { return [];