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..4a3906d
--- /dev/null
+++ b/.claude/skills/migrate-to-1.0/SKILL.md
@@ -0,0 +1,203 @@
+---
+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]`. The method itself then performs no attribute-driven
+CSRF check; the form may still enforce CSRF if it uses `SetAntiCsrfTrait`
+(see "Out of scope" below).
+
+### 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.
+- Deciding whether `#[CsrfProtection]` added by Step 1b is redundant. Forms
+ that already use `SetAntiCsrfTrait` enforce CSRF via `AbstractForm::apply()`
+ regardless of the attribute, so the attribute Step 1b inserts is harmless
+ but unnecessary. Flag these to the user so they can drop the attribute if
+ they prefer a single declaration site.
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..9bd9501
--- /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 | cs2pr
diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
new file mode 100644
index 0000000..71f9c1f
--- /dev/null
+++ b/.github/workflows/continuous-integration.yml
@@ -0,0 +1,57 @@
+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'
+ - '8.5'
+ 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..1c3a063
--- /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 || '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.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 || '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: 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 || '8.4' }}
+
+ - 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 || '8.4' }}
+ 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/.gitignore b/.gitignore
index 59370d4..93dec00 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,7 @@ build
vendor/
composer.phar
composer.lock
+tests/tmp/*
+!tests/tmp/.placefolder
+.phpunit.result.cache
+.phpcs-cache
diff --git a/.markdownlint.json b/.markdownlint.json
new file mode 100644
index 0000000..5ea4e95
--- /dev/null
+++ b/.markdownlint.json
@@ -0,0 +1,5 @@
+{
+ "MD046": {
+ "style": "fenced"
+ }
+}
diff --git a/.php_cs b/.php_cs
deleted file mode 100644
index 3518e7e..0000000
--- a/.php_cs
+++ /dev/null
@@ -1,132 +0,0 @@
-setRiskyAllowed(true)
- ->setRules(array(
- '@PSR2' => true,
- 'header_comment' => ['header' => $header, 'commentType' => 'PHPDoc', 'separate' => 'none'],
- 'array_syntax' => ['syntax' => 'short'],
- 'binary_operator_spaces' => ['align_equals' => false, 'align_double_arrow' => false],
- 'blank_line_after_opening_tag' => true,
- 'blank_line_after_namespace' => false,
- 'blank_line_before_return' => true,
- '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,
- 'function_typehint_space' => true,
- 'general_phpdoc_annotation_remove' => true,
- 'hash_to_slash_comment' => true,
- 'heredoc_to_nowdoc' => true,
- 'include' => true,
- 'indentation_type' => true,
- 'is_null' => ['use_yoda_style' => false],
- 'linebreak_after_opening_tag' => true,
- 'lowercase_cast' => true,
-// 'mb_str_functions' => true,
- 'method_separation' => true,
- 'modernize_types_casting' => true,
- 'native_function_casing' => true,
-// 'native_function_invocation' => true,
- 'new_with_braces' => false, //
- 'no_alias_functions' => true,
- 'no_blank_lines_after_class_opening' => true,
- 'no_blank_lines_after_phpdoc' => true,
- 'no_blank_lines_before_namespace' => true,
- '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_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,
- 'no_php4_constructor' => false,
- 'no_short_bool_cast' => true,
- 'no_short_echo_tag' => 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_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' => true,
- 'phpdoc_no_access' => true,
- 'phpdoc_no_alias_tag' => ['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,
- 'psr0' => true,
- 'psr4' => 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('src-data')
- ->exclude('src-deprecated')
- ->in(__DIR__)
- )->setLineEnding("\n")
- ->setUsingCache(false);
diff --git a/.scrutinizer.yml b/.scrutinizer.yml
index 0ca2b93..9c4042c 100644
--- a/.scrutinizer.yml
+++ b/.scrutinizer.yml
@@ -1,19 +1,12 @@
+build:
+ nodes:
+ analysis:
+ tests:
+ override:
+ - php-scrutinizer-run
+ environment:
+ php:
+ version: 8.2
+
filter:
paths: ["src/*"]
-tools:
- external_code_coverage: true
- php_code_coverage:
- timeout: 1200
- php_sim: true
- php_mess_detector: true
- php_pdepend: true
- php_analyzer: true
- php_cpd: true
- php_mess_detector:
- enabled: true
- config:
- ruleset: ./phpmd.xml
- php_code_sniffer:
- enabled: true
- config:
- ruleset: ./phpcs.xml
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 9c343aa..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-language: php
-sudo: false
-php:
- - 5.6
- - 7
- - 7.1
- - hhvm
-cache:
- directories:
- - vendor
- - $HOME/.composer/cache
-env:
- matrix:
- - DEPENDENCIES=""
- - DEPENDENCIES="--prefer-lowest --prefer-stable"
-before_script:
- - composer self-update
- - composer update $DEPENDENCIES
-script:
- - ./vendor/bin/phpunit --coverage-clover=coverage.clover;
- - if [ "$TRAVIS_PHP_VERSION" = "7.1" ]; then wget http://cs.sensiolabs.org/download/php-cs-fixer-v2.phar && php php-cs-fixer-v2.phar fix --config=.php_cs -v --dry-run --using-cache=no --path-mode=intersection `git diff --name-only --diff-filter=ACMRTUXB $TRAVIS_COMMIT_RANGE`; fi
-
-after_script:
- - if [ "$TRAVIS_PHP_VERSION" = "7.1" ]; then wget https://scrutinizer-ci.com/ocular.phar && php ocular.phar code-coverage:upload --format=php-clover coverage.clover; fi
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..27a07e9
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,94 @@
+# 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.1] - 2026-05-19
+
+### Added
+
+- `Ray\WebFormModule\WebFormModule` — module class whose name matches the
+ package and namespace. Use this in new code.
+
+### Deprecated
+
+- `Ray\WebFormModule\AuraInputModule` is now a thin subclass of
+ `WebFormModule` kept for backwards compatibility. Existing applications
+ that install `new AuraInputModule()` continue to work without changes,
+ but should migrate to `new WebFormModule()`.
+
+## [1.0.0] - 2026-05-17
+
+### Changed
+
+- 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]`.
+- **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
+ `ReflectionMethod::getAttributes()`.
+- **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`,
+ `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.1]: https://github.com/ray-di/Ray.WebFormModule/compare/1.0.0...1.0.1
+[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 b3acce9..278b46a 100644
--- a/README.JA.md
+++ b/README.JA.md
@@ -1,8 +1,8 @@
# Ray.WebFormModule
+[](https://github.com/ray-di/Ray.WebFormModule/actions/workflows/continuous-integration.yml)
+[](https://github.com/ray-di/Ray.WebFormModule/actions/workflows/coding-standards.yml)
[](https://scrutinizer-ci.com/g/ray-di/Ray.WebFormModule/?branch=1.x)
-[](https://scrutinizer-ci.com/g/ray-di/Ray.WebFormModule/?branch=1.x)
-[](https://travis-ci.org/ray-di/Ray.WebFormModule)
Ray.WebFormModuleはアスペクト指向でフォームのバリデーションを行うモジュールです。
フォームライブラリには[Aura.Input](https://github.com/auraphp/Aura.Input)を使い、
@@ -18,16 +18,19 @@ Ray.WebFormModuleはアスペクト指向でフォームのバリデーション
```php
use Ray\Di\AbstractModule;
-use Ray\WebFormModule\AuraInputModule;
+use Ray\WebFormModule\WebFormModule;
class AppModule extends AbstractModule
{
protected function configure()
{
- $this->install(new AuraInputModule);
+ $this->install(new WebFormModule());
}
}
```
+
+> 互換性のため `Ray\WebFormModule\AuraInputModule` クラスも `WebFormModule` の薄い
+> サブクラスとして残されています。新規コードでは `WebFormModule` を使ってください。
## Usage
### Form
@@ -79,7 +82,7 @@ public function createAction($id, $name, $body)
### Controller
-コントローラークラスにフォームをインジェクトします。フォームのバリデーションを行うメソッドを`@FormValidation`で
+コントローラークラスにフォームをインジェクトします。フォームのバリデーションを行うメソッドを`#[FormValidation]`で
アノテートします。この時フォームのプロパティ名を`form`で、バリデーションが失敗したときのメソッドを`onFailure`で指定します。
```php
@@ -95,18 +98,13 @@ class MyController
*/
protected $contactForm;
- /**
- * @Inject
- * @Named("contact_form")
- */
- public function setForm(FormInterface $form)
+ #[Inject]
+ public function setForm(#[Named("contact_form")] FormInterface $form)
{
$this->contactForm = $form;
}
- /**
- * @FormValidation(form="contactForm", onFailure="badRequestAction")
- */
+ #[FormValidation(form: "contactForm", onFailure: "badRequestAction")]
public function createAction()
{
// validation success
@@ -129,31 +127,87 @@ class MyController
### CSRF Protections
-CSRF対策を行うためにはフォームにCSRFオブジェクトをセットします。
+CSRF対策は **opt-in** で、独立した 2 つの経路のいずれかで有効化できます。
+
+- **フォーム単位**: フォームに `use SetAntiCsrfTrait;` を追加します。
+ Ray.Di がトレイトの `#[Inject]` setter 経由で `AntiCsrfInterface` を注入し、
+ `postConstruct()` でトークンフィールドが追加され、`apply()` の呼び出しごとに
+ トークンが検証されます。
+- **アクション単位**: バリデーション対象のコントローラメソッドに
+ `#[CsrfProtection]` を付与します。`AuraInputInterceptor` が `apply()` 実行前に
+ `AntiCsrfInterface` をフォームへ注入します。
+
+どちらの経路でも、トークン不一致時には `AbstractForm::apply()` が
+`CsrfViolationException` を throw します。どちらも使わない場合は CSRF 検証は
+行われません。両方を併用しても害はありませんが冗長なので、用途に合わせて
+どちらか一方を選んでください。
+
+アクション単位 — コントローラのメソッドで宣言:
```php
+use Ray\WebFormModule\Annotation\CsrfProtection;
+use Ray\WebFormModule\Annotation\FormValidation;
+
+class MyController
+{
+ #[FormValidation(form: "contactForm")]
+ #[CsrfProtection]
+ public function createAction()
+ {
+ }
+}
+```
+
+フォーム単位 — フォーム自身で宣言:
+
+```php
+use Ray\WebFormModule\AbstractForm;
use Ray\WebFormModule\SetAntiCsrfTrait;
-class MyForm extends AbstractAuraForm
+class MyForm extends AbstractForm
{
use SetAntiCsrfTrait;
+}
```
セキュリティレベルを高めるためにはユーザーの認証を含んだカスタム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) を参照してください。
+
+### 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アプリケーションなどに便利です。
+`#[FormValidation]`の代わりに`#[InputValidation]`とアノテートするとバリデーションが失敗したときに`Ray\WebFormModule\Exception\ValidationException`が投げられるよになります。この場合はHTML表現は使われません。Web APIアプリケーションなどに便利です。
```php
use Ray\WebFormModule\Annotation\InputValidation;
class Foo
{
- /**
- * @InputValidation(form="form1")
- */
+ #[InputValidation(form: "form1")]
public function createAction($name)
{
// ...
@@ -168,8 +222,8 @@ class FakeVndErrorModule extends AbstractModule
{
protected function configure()
{
- $this->install(new AuraInputModule);
- $this->override(new FormVndErrorModule);
+ $this->install(new WebFormModule());
+ $this->override(new FormVndErrorModule());
}
```
@@ -190,17 +244,11 @@ echo $e->error;
//}
```
-`@VndError`アノテーションで`vnd.error+json`に必要な情報を加えることができます。
+`#[VndError]`属性で`vnd.error+json`に必要な情報を加えることができます。
```php
- /**
- * @FormValidation(form="contactForm")
- * @VndError(
- * message="foo validation failed",
- * logref="a1000", path="/path/to/error",
- * href={"_self"="/path/to/error", "help"="/path/to/help"}
- * )
- */
+ #[FormValidation(form: "contactForm")]
+ #[VndError(message: "foo validation failed", logref: "a1000", path: "/path/to/error", href: ["_self" => "/path/to/error", "help" => "/path/to/help"])]
```
このオプションのモジュールはAPIアプリケーションの時に有用です。
diff --git a/README.md b/README.md
index 2265909..fddc5e8 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
# Ray.WebFormModule
+[](https://github.com/ray-di/Ray.WebFormModule/actions/workflows/continuous-integration.yml)
+[](https://github.com/ray-di/Ray.WebFormModule/actions/workflows/coding-standards.yml)
[](https://scrutinizer-ci.com/g/ray-di/Ray.WebFormModule/?branch=1.x)
-[](https://scrutinizer-ci.com/g/ray-di/Ray.WebFormModule/?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,22 +12,26 @@ 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
```php
use Ray\Di\AbstractModule;
-use Ray\WebFormModule\AuraInputModule;
+use Ray\WebFormModule\WebFormModule;
class AppModule extends AbstractModule
{
protected function configure()
{
- $this->install(new AuraInputModule);
+ $this->install(new WebFormModule());
}
}
```
+
+> The legacy `Ray\WebFormModule\AuraInputModule` class is still available as a thin
+> subclass of `WebFormModule` for backwards compatibility. New code should prefer
+> `WebFormModule`.
## Usage
### Form class
@@ -88,7 +92,7 @@ class MyForm extends AbstractForm
```
### Controller
-We annotate the methods which web form validation is required with `@FormValidation`. We can specify form object property name with `name` and failiure method name with `@onFailure`.
+We annotate the methods which web form validation is required with `#[FormValidation]`. We can specify form object property name with `form` and failure method name with `onFailure`.
```php
use Ray\Di\Di\Inject;
@@ -103,21 +107,17 @@ class MyController
*/
protected $contactForm;
- /**
- * @Inject
- * @Named("contact_form")
- */
- public function setForm(FormInterface $form)
+ #[Inject]
+ public function setForm(#[Named("contact_form")] FormInterface $form)
{
$this->contactForm = $form;
}
- /**
- * @FormValidation(form="contactForm", onFailure="badRequestAction")
- */
+ #[FormValidation(form: "contactForm", onFailure: "badRequestAction")]
public function createAction()
{
// validation success
+ // More detail for `vnd.error+json` can be added with `#[VndError]`.
}
public function badRequestAction()
@@ -140,17 +140,79 @@ 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** and can be enabled through either of two
+independent paths:
+
+- **Per-form**: add `use SetAntiCsrfTrait;` to the form. `AntiCsrfInterface`
+ is injected by Ray.Di through the trait's `#[Inject]` setter, the token
+ field is added in `postConstruct()`, and every `apply()` call verifies
+ the token.
+- **Per-action**: annotate the validated controller method with
+ `#[CsrfProtection]`. `AuraInputInterceptor` then injects
+ `AntiCsrfInterface` into the form before `apply()` runs.
+
+Either path causes `AbstractForm::apply()` to throw `CsrfViolationException`
+on token mismatch. Without either path, no CSRF check is performed. Combining
+both paths is harmless but redundant — pick whichever fits your use case.
+
+Per-action — declare CSRF on the controller method:
```php
+use Ray\WebFormModule\Annotation\CsrfProtection;
+use Ray\WebFormModule\Annotation\FormValidation;
+
+class MyController
+{
+ #[FormValidation(form: "contactForm")]
+ #[CsrfProtection]
+ public function createAction()
+ {
+ }
+}
+```
+
+Per-form — declare CSRF on the form itself:
+
+```php
+use Ray\WebFormModule\AbstractForm;
use Ray\WebFormModule\SetAntiCsrfTrait;
-class MyController
+class MyForm extends AbstractForm
{
use SetAntiCsrfTrait;
+}
```
+
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.
+
+### 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,
@@ -162,8 +224,8 @@ class FakeVndErrorModule extends AbstractModule
{
protected function configure()
{
- $this->install(new AuraInputModule);
- $this->override(new FormVndErrorModule);
+ $this->install(new WebFormModule());
+ $this->override(new FormVndErrorModule());
}
```
A `Ray\WebFormModule\Exception\ValidationException` will be thrown.
@@ -183,21 +245,15 @@ echo $e->error;
//}
```
-More detail for `vnd.error+json`can be add with `@VndError` annotation.
+More detail for `vnd.error+json` can be added with the `#[VndError]` attribute.
```php
- /**
- * @FormValidation(form="contactForm")
- * @VndError(
- * message="foo validation failed",
- * logref="a1000", path="/path/to/error",
- * href={"_self"="/path/to/error", "help"="/path/to/help"}
- * )
- */
+ #[FormValidation(form: "contactForm")]
+ #[VndError(message: "foo validation failed", logref: "a1000", path: "/path/to/error", href: ["_self" => "/path/to/error", "help" => "/path/to/help"])]
```
This optional module is handy for API application.
-### Demo
+## Demo
$ php -S docs/demo/1.csrf/web.php
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 3caf3ff..6f8b471 100644
--- a/composer.json
+++ b/composer.json
@@ -6,14 +6,23 @@
"Ray.Di module"
],
"require": {
- "ray/di": "^2.5",
+ "php": ">=8.0.0",
+ "ray/di": "^2.16",
+ "ray/aop": "^2.14",
"aura/input": "^1.2",
"aura/filter": "^2.3|3.x-dev",
- "aura/html": "^2.4",
- "ray/aura-session-module": "^1.0"
+ "aura/html": "^2.5",
+ "aura/session": "^2.1 || ^4.0",
+ "ray/aura-session-module": "^1.1"
},
"require-dev": {
- "phpunit/phpunit": "^5.7.13"
+ "phpunit/phpunit": "^9.5",
+ "doctrine/coding-standard": "^14.0",
+ "phpstan/phpstan": "^1.10",
+ "squizlabs/php_codesniffer": "^4.0",
+ "vimeo/psalm": "^5.0 || ^6.0",
+ "phpmd/phpmd": "^2.13",
+ "maglnet/composer-require-checker": "^4.0"
},
"license": "MIT",
"autoload":{
@@ -28,8 +37,20 @@
},
"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": "phpcs",
+ "cs-fix": "phpcbf",
+ "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": {
+ "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 ce91165..aea6dbd 100644
--- a/docs/demo/1.csrf/ContactForm.php
+++ b/docs/demo/1.csrf/ContactForm.php
@@ -1,9 +1,4 @@
response['code'] = 201;
diff --git a/docs/demo/1.csrf/MyModule.php b/docs/demo/1.csrf/MyModule.php
index 295da43..4dd9905 100644
--- a/docs/demo/1.csrf/MyModule.php
+++ b/docs/demo/1.csrf/MyModule.php
@@ -1,9 +1,4 @@
install(new AuraInputModule);
+ $this->install(new WebFormModule());
$this->bind(FormInterface::class)->annotatedWith('contact_form')->to(ContactForm::class);
}
}
diff --git a/docs/demo/1.csrf/run.php b/docs/demo/1.csrf/run.php
index 81e7c17..9741b75 100644
--- a/docs/demo/1.csrf/run.php
+++ b/docs/demo/1.csrf/run.php
@@ -1,9 +1,5 @@
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 5bf307b..ef72c18 100644
--- a/docs/demo/1.csrf/web.php
+++ b/docs/demo/1.csrf/web.php
@@ -1,9 +1,5 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ src
+ tests
+ */tmp/*
+ */Fake/*
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..00b655a
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,23 @@
+parameters:
+ level: max
+ reportUnmatchedIgnoredErrors: false
+ paths:
+ - src
+ ignoreErrors:
+ # PHPMD annotations (@SuppressWarnings(PHPMD.X)) are not PHPStan tags.
+ -
+ identifier: phpDoc.parseError
+ message: '#@SuppressWarnings#'
+ # Aura\Html\HelperLocator exposes helpers via __get/__call magic methods.
+ -
+ identifier: method.notFound
+ message: '#Aura\\Html\\HelperLocator::(input|form)#'
+ path: src/AbstractForm.php
+ # SetAntiCsrfTrait is part of the public API for consumers to compose.
+ -
+ identifier: trait.unused
+ path: src/SetAntiCsrfTrait.php
+ # AbstractForm narrows Fieldset::$filter (FilterInterface) to SubjectFilter for typed filter access.
+ -
+ identifier: property.phpDocType
+ message: '#AbstractForm::\$filter#'
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 0c37fb9..ca6b708 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,19 +1,21 @@
-
+
tests
+
+
+ src
+
+
+
+
+
+
+
-
-
-
+
-
-
-
- src
-
-
diff --git a/psalm.xml b/psalm.xml
new file mode 100644
index 0000000..cd73652
--- /dev/null
+++ b/psalm.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AbstractForm.php b/src/AbstractForm.php
index 3ab127e..4a0431f 100644
--- a/src/AbstractForm.php
+++ b/src/AbstractForm.php
@@ -1,41 +1,45 @@
>|null */
+ protected array|null $errorMessages = null;
+ protected HelperLocator $helper;
+ protected AntiCsrfInterface|null $antiCsrf = null;
public function __construct()
{
@@ -48,56 +52,82 @@ public function __clone()
}
/**
- * @param BuilderInterface $builder
- * @param FilterFactory $filterFactory
- * @param HelperLocatorFactory $helperFactory
- *
- * @\Ray\Di\Di\Inject
+ * Return form markup string
*/
+ public function __toString(): string
+ {
+ try {
+ if (! $this instanceof ToStringInterface) {
+ throw new LogicException(ToStringInterface::class . ' is not implemented');
+ }
+
+ return $this->toString();
+ } catch (Throwable $e) {
+ trigger_error($e->getMessage() . PHP_EOL . $e->getTraceAsString(), E_USER_ERROR);
+ }
+
+ // Reachable when a custom error handler intercepts E_USER_ERROR without halting.
+ return ''; // @phpstan-ignore deadCode.unreachable
+ }
+
+ #[Inject]
public function setBaseDependencies(
BuilderInterface $builder,
FilterFactory $filterFactory,
- HelperLocatorFactory $helperFactory
- ) {
+ HelperLocatorFactory $helperFactory,
+ ): void {
+ assert($builder instanceof Builder);
$this->builder = $builder;
$this->filter = $filterFactory->newSubjectFilter();
$this->helper = $helperFactory->newInstance();
}
- public function setAntiCsrf(AntiCsrfInterface $antiCsrf)
+ public function setAntiCsrf(AntiCsrfInterface $antiCsrf): void
{
$this->antiCsrf = $antiCsrf;
}
- /**
- * @\Ray\Di\Di\PostConstruct
- */
- public function postConstruct()
+ 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
{
$this->init();
if ($this->antiCsrf instanceof AntiCsrfInterface) {
- $this->antiCsrf->setField($this);
+ $this->enableAntiCsrf($this->antiCsrf);
}
}
- /**
- * {@inheritdoc}
- */
- public function input($input)
+ /** {@inheritDoc} */
+ 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($input)
+ /** {@inheritDoc} */
+ public function error(string $input): string
{
- if (! $this->errorMessages) {
+ if ($this->errorMessages === null) {
+ /** @var 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])) {
@@ -108,18 +138,19 @@ public function error($input)
}
/**
- * @param array $attr attributes for the form tag
+ * @param array $attr attributes for the form tag
*
- * @throws \Aura\Html\Exception\HelperNotFound
- * @throws \Aura\Input\Exception\NoSuchInput
- *
- * @return string
+ * @throws NoSuchInput
+ * @throws HelperNotFound
*/
- public function form($attr = [])
+ 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;
@@ -128,31 +159,36 @@ public function form($attr = [])
/**
* Applies the filter to a subject.
*
- * @param array $data
+ * @param array $data
*
* @throws CsrfViolationException
- *
- * @return bool
*/
- public function apply(array $data)
+ public function apply(array $data): bool
{
if ($this->antiCsrf && ! $this->antiCsrf->isValid($data)) {
- throw new CsrfViolationException;
+ throw new CsrfViolationException();
}
+
$this->fill($data);
- $isValid = $this->filter->apply($data);
- return $isValid;
+ return $this->filter->apply($data);
}
/**
* Returns all failure messages for all fields.
*
- * @return array
+ * @return array>
*/
- public function getFailureMessages()
+ public function getFailureMessages(): array
{
- $messages = $this->filter->getFailures()->getMessages();
+ /** @var FailureCollection|null $failure */
+ $failure = $this->filter->getFailures();
+ if ($failure === null) {
+ return [];
+ }
+
+ /** @var array> $messages */
+ $messages = $failure->getMessages();
return $messages;
}
@@ -160,10 +196,10 @@ public function getFailureMessages()
/**
* Returns all the fields collection
*
- * @return \ArrayIterator
+ * @return ArrayIterator
*/
- public function getIterator()
+ public function getIterator(): ArrayIterator
{
- return new \ArrayIterator($this->inputs);
+ return new ArrayIterator($this->inputs);
}
}
diff --git a/src/Annotation/AbstractValidation.php b/src/Annotation/AbstractValidation.php
index 3cf5e2f..8669808 100644
--- a/src/Annotation/AbstractValidation.php
+++ b/src/Annotation/AbstractValidation.php
@@ -1,19 +1,12 @@
$href
*
* @see http://www.w3.org/TR/html5/links.html#link-type-help
- *
- * about
* @see http://tools.ietf.org/html/rfc6903#section-2
- *
- * describes
* @see http://tools.ietf.org/html/rfc6892
*/
- public $path;
+ public function __construct(
+ public string $message = '',
+ public array $href = [],
+ public string|null $logref = null,
+ public string|null $path = null,
+ ) {
+ }
}
diff --git a/src/AntiCsrf.php b/src/AntiCsrf.php
index 478ec0e..df4f16d 100644
--- a/src/AntiCsrf.php
+++ b/src/AntiCsrf.php
@@ -1,68 +1,48 @@
session = $session;
$this->isCli = is_bool($isCli) ? $isCli : PHP_SAPI === 'cli';
}
- public function setField(Fieldset $fieldset)
+ public function setField(Fieldset $fieldset): void
{
$fieldset->setField(self::TOKEN_KEY, 'hidden')
- ->setAttribs(['value' => $this->getToken()]);
+ ->setAttribs(['value' => $this->getToken()]);
}
- /**
- * @param array $data
- *
- * @return bool
- */
- public function isValid(array $data)
+ /** @param array $data */
+ public function isValid(array $data): bool
{
if ($this->isCli) {
return true;
}
- return isset($data[self::TOKEN_KEY]) && $data[self::TOKEN_KEY] == $this->getToken();
+ return isset($data[self::TOKEN_KEY]) && $data[self::TOKEN_KEY] === $this->getToken();
}
- /**
- * @return string
- */
- private function getToken()
+ private function getToken(): string
{
- $value = $this->isCli ? self::TEST_TOKEN : $this->session->getCsrfToken()->getValue();
-
- return $value;
+ return $this->isCli ? self::TEST_TOKEN : $this->session->getCsrfToken()->getValue();
}
}
diff --git a/src/AuraInputInterceptor.php b/src/AuraInputInterceptor.php
index 48a38d0..676ae74 100644
--- a/src/AuraInputInterceptor.php
+++ b/src/AuraInputInterceptor.php
@@ -1,56 +1,63 @@
reader = $reader;
$this->failureHandler = $handler;
}
+ #[Inject]
+ public function setAntiCsrf(AntiCsrfInterface $antiCsrf): void
+ {
+ $this->antiCsrf = $antiCsrf;
+ }
+
/**
- * {@inheritdoc}
+ * {@inheritDoc}
+ *
+ * @param MethodInvocation