├── .github ├── dependabot.yml ├── labeler.yml ├── release.yml ├── settings.yml └── workflows │ ├── auto-label-prs.yml │ ├── automatic-release.yml │ ├── automerge-dependabot.yml │ ├── continuous-integration.yml │ ├── conventional-pr-title.yml │ └── merged-notification.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── config.yml ├── docs ├── _index.md ├── deploy.sh └── latest │ ├── _index.md │ ├── api-attributes.md │ ├── api-form-elements.md │ ├── customizing-the-inputfilter.md │ ├── example-expressive-action.md │ ├── example-expressive-custom-validators.md │ ├── example-password-confirmation.md │ ├── example-recaptcha.md │ ├── example-symfony-action.md │ └── known-issues.md ├── go.mod ├── phpcs.xml.dist ├── phpunit.xml.dist ├── renovate.json ├── src ├── ConfigProvider.php ├── Form.php ├── FormElement │ ├── BaseFormElement.php │ ├── Checkbox.php │ ├── Color.php │ ├── Date.php │ ├── DateTime.php │ ├── Email.php │ ├── File.php │ ├── Hidden.php │ ├── Month.php │ ├── Number.php │ ├── Password.php │ ├── Radio.php │ ├── Range.php │ ├── Select.php │ ├── Tel.php │ ├── Text.php │ ├── Textarea.php │ ├── Time.php │ ├── Url.php │ └── Week.php ├── FormFactory.php ├── FormFactoryFactory.php ├── FormFactoryInterface.php ├── FormInterface.php ├── InputFilterFactory.php ├── ValidationResult.php ├── ValidationResultInterface.php └── Validator │ └── RecaptchaValidator.php └── test ├── Fixtures ├── SAMPLE ├── attribute-aria-required.test ├── attribute-data-filters.test ├── attribute-data-input-name.test ├── attribute-data-validators-exception.test ├── attribute-data-validators.test ├── attribute-disabled.test ├── attribute-maxlength.test ├── attribute-minlength.test ├── attribute-pattern.test ├── attribute-required.test ├── form-contact-example.test ├── form-render-error.test ├── form-render-errors.test ├── select-render.test ├── select.test ├── textarea.test ├── type-checkbox.test ├── type-color.test ├── type-date.test ├── type-datetime-local.test ├── type-email.test ├── type-file.test ├── type-month.test ├── type-number.test ├── type-password.test ├── type-radio.test ├── type-search.test ├── type-tel.test ├── type-text.test ├── type-time.test ├── type-url.test ├── type-week.test └── utf8-encoding.test ├── FormElement └── BaseFormElementTest.php ├── FormElementArraytest.php ├── FormElementsTest.php ├── FormFactoryFactoryTest.php ├── FormFactoryTest.php ├── FormTest.php ├── InputFilterFactoryTest.php ├── ValidationResultTest.php └── Validator └── RecaptchaValidatorTest.php /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | open-pull-requests-limit: 20 9 | labels: 10 | - "dependencies" 11 | 12 | - package-ecosystem: composer 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | open-pull-requests-limit: 20 17 | allow: 18 | - dependency-type: "all" 19 | labels: 20 | - "dependencies" 21 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | ci: 2 | - .github/* 3 | - .github/**/* 4 | - .circleci/* 5 | - .circleci/**/* 6 | - .travis.yml 7 | 8 | build: 9 | - composer.json 10 | - composer.lock 11 | - package-lock.json 12 | - package.json 13 | - yarn.lock 14 | 15 | docs: 16 | - docs/* 17 | - docs/**/* 18 | 19 | feat: 20 | - src/* 21 | - src/**/* 22 | 23 | test: 24 | - phpunit.* 25 | - test/* 26 | - test/**/* 27 | - tests/* 28 | - tests/**/* 29 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: Features 4 | labels: 5 | - feat 6 | - enhancement 7 | - title: Bug Fixes 8 | labels: 9 | - bug 10 | - fix 11 | - title: Documentation 12 | labels: 13 | - doc 14 | - title: Performance 15 | labels: 16 | - perf 17 | - title: Refactor 18 | labels: 19 | - refactor 20 | - title: Styling 21 | labels: 22 | - style 23 | - title: Testing 24 | labels: 25 | - test 26 | - title: 🛡 Security Updates 27 | labels: 28 | - dependencies 29 | - security-update 30 | - title: Other Changes 31 | labels: 32 | - "*" 33 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | _extends: ".github" 2 | -------------------------------------------------------------------------------- /.github/workflows/auto-label-prs.yml: -------------------------------------------------------------------------------- 1 | name: Auto label Pull Requests 2 | 3 | on: 4 | pull_request_target: 5 | 6 | jobs: 7 | triage: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/labeler@main 11 | with: 12 | repo-token: '${{ secrets.GITHUB_TOKEN }}' 13 | -------------------------------------------------------------------------------- /.github/workflows/automatic-release.yml: -------------------------------------------------------------------------------- 1 | name: Automatic Releases 2 | 3 | on: 4 | milestone: 5 | types: 6 | - closed 7 | 8 | jobs: 9 | release: 10 | name: GIT tag, release & create merge-up PR 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Release 18 | uses: laminas/automatic-releases@v1 19 | with: 20 | command-name: laminas:automatic-releases:release 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.ORGANIZATION_ADMIN_TOKEN }} 23 | SIGNING_SECRET_KEY: ${{ secrets.SIGNING_SECRET_KEY }} 24 | GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }} 25 | GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }} 26 | 27 | - name: Create Merge-Up Pull Request 28 | uses: laminas/automatic-releases@v1 29 | with: 30 | command-name: laminas:automatic-releases:create-merge-up-pull-request 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | SIGNING_SECRET_KEY: ${{ secrets.SIGNING_SECRET_KEY }} 34 | GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }} 35 | GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }} 36 | 37 | - name: Create and/or Switch to new Release Branch 38 | uses: laminas/automatic-releases@v1 39 | with: 40 | command-name: laminas:automatic-releases:switch-default-branch-to-next-minor 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.ORGANIZATION_ADMIN_TOKEN }} 43 | SIGNING_SECRET_KEY: ${{ secrets.SIGNING_SECRET_KEY }} 44 | GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }} 45 | GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }} 46 | 47 | - name: Create new milestones 48 | uses: laminas/automatic-releases@v1 49 | with: 50 | command-name: laminas:automatic-releases:create-milestones 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | SIGNING_SECRET_KEY: ${{ secrets.SIGNING_SECRET_KEY }} 54 | GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }} 55 | GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }} 56 | -------------------------------------------------------------------------------- /.github/workflows/automerge-dependabot.yml: -------------------------------------------------------------------------------- 1 | # See https://github.com/ridedott/merge-me-action/ 2 | # This workflow automates merges from patches sent by Dependabot, and 3 | # only by dependabot, once the other CI workflows pass 4 | name: automerge-dependabot 5 | 6 | on: 7 | workflow_run: 8 | types: 9 | - completed 10 | workflows: 11 | - "Continuous Integration" 12 | 13 | jobs: 14 | automerge: 15 | name: Auto-merge Dependabot PRs 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Auto merge 19 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 20 | uses: ridedott/merge-me-action@v2 21 | with: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | MERGE_METHOD: SQUASH 24 | PRESET: DEPENDABOT_MINOR 25 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | tags: 8 | 9 | jobs: 10 | matrix: 11 | name: Generate job matrix 12 | runs-on: ubuntu-latest 13 | outputs: 14 | matrix: ${{ steps.matrix.outputs.matrix }} 15 | steps: 16 | - name: Gather CI configuration 17 | id: matrix 18 | uses: laminas/laminas-ci-matrix-action@v1 19 | 20 | qa: 21 | name: QA Checks 22 | needs: [matrix] 23 | runs-on: ${{ matrix.operatingSystem }} 24 | strategy: 25 | fail-fast: false 26 | matrix: ${{ fromJSON(needs.matrix.outputs.matrix) }} 27 | steps: 28 | - name: ${{ matrix.name }} 29 | uses: laminas/laminas-continuous-integration-action@v1 30 | with: 31 | job: ${{ matrix.job }} 32 | -------------------------------------------------------------------------------- /.github/workflows/conventional-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: Check PR title 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - reopened 8 | - edited 9 | - synchronize 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: aslafy-z/conventional-pr-title-action@master 16 | with: 17 | success-state: Title follows the conventional commit specification. 18 | failure-state: Title does not follow the conventional commit specification. 19 | context-name: conventional-pr-title 20 | preset: conventional-changelog-angular@latest 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/merged-notification.yml: -------------------------------------------------------------------------------- 1 | name: Merged notification 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - closed 7 | 8 | jobs: 9 | comment: 10 | if: github.event.pull_request.merged 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/github-script@v6 14 | with: 15 | github-token: ${{secrets.GITHUB_TOKEN}} 16 | script: | 17 | github.rest.issues.createComment({ 18 | issue_number: context.issue.number, 19 | owner: context.repo.owner, 20 | repo: context.repo.repo, 21 | body: 'Thanks for contributing!' 22 | }) 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .phpcs-cache 2 | .phpunit.result.cache 3 | composer.lock 4 | phpunit.xml 5 | vendor/ 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2021 Geert Eltink 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 2 | 3 | Thank you so much for being interested in this project! Open Source is rewarding, but it can also be exhausting. Therefor this code is provided as-is, and is currently not actively maintained. We invite you to peruse the code and even use it in your next project, provided you follow the included license! 4 | 5 | No guarantee of support for the code is provided, and there is no promise that pull requests will be reviewed or merged. It’s open source, so forking is allowed; just be sure to give credit where it’s due! 6 | 7 | --- 8 | 9 | As challenged by a [tweet](https://twitter.com/Ocramius/status/680817040429592576), this library extracts validation 10 | rules and filters from a html form and validates submitted user data against it. 11 | 12 | It's pretty crazy what you have to do to get a form build in frameworks. Create a lot of php classes for elements, 13 | validation, etc. So why not build a html form and use the standard element attributes to extract the validation rules 14 | and filters. Together with some powerful html compliant data attributes you can create forms, customize validation 15 | rules and filters in one place. 16 | 17 | ## Installation 18 | 19 | ```bash 20 | $ composer require xtreamwayz/html-form-validator 21 | ``` 22 | 23 | ## Documentation 24 | 25 | All project documentation is located in the [./docs](./docs) folder. If you would like to contribute 26 | to the documentation, please submit a pull request. You can read the docs online: 27 | https://xtreamwayz.github.io/html-form-validator/ 28 | 29 | ## Contributing 30 | 31 | **_BEFORE you start work on a feature or fix_**, please read & follow the 32 | [contributing guidelines](https://github.com/xtreamwayz/.github/blob/master/CONTRIBUTING.md#contributing) 33 | to help avoid any wasted or duplicate effort. 34 | 35 | ## Copyright and license 36 | 37 | Code released under the [MIT License](https://github.com/xtreamwayz/.github/blob/master/LICENSE.md). 38 | Documentation distributed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). 39 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xtreamwayz/html-form-validator", 3 | "type": "library", 4 | "description": "A library validating and filtering submitted data by reusing html form attributes", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Geert Eltink", 9 | "homepage": "https://github.com/geerteltink" 10 | } 11 | ], 12 | "require": { 13 | "php": "^7.4 || ^8.0", 14 | "ext-dom": "*", 15 | "ext-libxml": "*", 16 | "laminas/laminas-filter": "^2.13", 17 | "laminas/laminas-i18n": "^2.13", 18 | "laminas/laminas-inputfilter": "^2.13", 19 | "laminas/laminas-servicemanager": "^3.10", 20 | "laminas/laminas-stdlib": "^3.6", 21 | "laminas/laminas-uri": "^2.9", 22 | "laminas/laminas-validator": "^2.15", 23 | "psr/container": "^1.0 || ^2.0", 24 | "psr/http-message": "^1.0" 25 | }, 26 | "require-dev": { 27 | "laminas/laminas-coding-standard": "^2.3", 28 | "phpspec/prophecy-phpunit": "^2.0", 29 | "phpstan/phpstan": "^1.4", 30 | "phpunit/phpunit": "^9.5" 31 | }, 32 | "suggest": { 33 | "laminas/laminas-servicemanager": "To support third-party validators and filters" 34 | }, 35 | "config": { 36 | "sort-packages": true 37 | }, 38 | "extra": { 39 | "laminas": { 40 | "config-provider": "Xtreamwayz\\HTMLFormValidator\\ConfigProvider" 41 | } 42 | }, 43 | "autoload": { 44 | "psr-4": { 45 | "Xtreamwayz\\HTMLFormValidator\\": "src/" 46 | } 47 | }, 48 | "autoload-dev": { 49 | "psr-4": { 50 | "XtreamwayzTest\\HTMLFormValidator\\": "test/" 51 | } 52 | }, 53 | "scripts": { 54 | "analyse": "phpstan analyse --level=0 src test", 55 | "analyse-strict": "phpstan analyse -l 7 src", 56 | "check": [ 57 | "@cs-check", 58 | "@test", 59 | "@analyse" 60 | ], 61 | "cs-check": "phpcs", 62 | "cs-fix": "phpcbf", 63 | "test": "phpunit --colors=always", 64 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml" 65 | }, 66 | "support": { 67 | "issues": "https://github.com/xtreamwayz/html-form-validator/issues", 68 | "forum": "https://github.com/xtreamwayz/community/discussions", 69 | "source": "https://github.com/xtreamwayz/html-form-validator", 70 | "docs": "https://xtreamwayz.github.io/html-form-validator/" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | params: 2 | html-form-validator: 3 | name: html-form-validator 4 | latest: v1 5 | versions: 6 | - v1 7 | -------------------------------------------------------------------------------- /docs/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: xtreamwayz/html-form-validator 3 | type: project 4 | layout: landingpage 5 | project: html-form-validator 6 | --- 7 | 8 | As challenged by a [tweet](https://twitter.com/Ocramius/status/680817040429592576), this library extracts validation 9 | rules and filters from a html form and validates submitted user data against it. 10 | 11 | It's pretty crazy what you have to do to get a form build in frameworks. Create a lot of php classes for elements, 12 | validation, etc. So why not build a html form and use the standard element attributes to extract the validation rules 13 | and filters. Together with some powerful html compliant data attributes you can create forms, customize validation 14 | rules and filters in one place. 15 | -------------------------------------------------------------------------------- /docs/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Deploy documentation to the github wiki 3 | # 4 | # Environment variables that may be of use: 5 | # 6 | # - GH_USER_NAME indicates the GitHub author name to use; 7 | # - GH_USER_EMAIL indicates the email address for that author; 8 | # - GH_REPO indicates the GitHub / location; 9 | # - GH_TOKEN is the personal security token to use for commits. 10 | # 11 | # All of the above are exported via the project .travis.yml file (with 12 | # GH_TOKEN being encrypted and present in the `secure` key). The user details 13 | # need to match the token used for this to work. 14 | # 15 | # The script should be run from the project root. 16 | 17 | if [[ "$TRAVIS_PULL_REQUEST" != "false" ]]; then 18 | echo "This is a pull request. No deployment will be done." 19 | exit 0 20 | fi 21 | if [[ "$TRAVIS_BRANCH" != "master" ]]; then 22 | echo "Testing on a branch other than master. No deployment will be done." 23 | exit 0 24 | fi 25 | if [[ "$DEPLOY_DOCS" != "true" ]]; then 26 | echo "Ignoring deployment. No deployment will be done." 27 | exit 0 28 | fi 29 | 30 | echo "... preparing deployment" 31 | 32 | # Get curent commit revision 33 | rev=$(git rev-parse --short HEAD) 34 | 35 | echo "... revision: ${rev}" 36 | 37 | # Initialize gh-pages checkout 38 | mkdir -p ${TRAVIS_BUILD_DIR}/docs/build 39 | ( 40 | echo "... setup build dir" 41 | cd ${TRAVIS_BUILD_DIR}/docs/build 42 | git init 43 | git config user.name "Travis-CI" 44 | git config user.email "${GH_USER_EMAIL}" 45 | git remote add upstream "https://${GH_TOKEN}@github.com/${GH_REPO}.wiki.git" 46 | git fetch upstream 47 | git reset upstream/master 48 | ) 49 | 50 | # Build the documentation 51 | echo "... copying docs" 52 | cp -rf ${TRAVIS_BUILD_DIR}/docs/wiki/* ${TRAVIS_BUILD_DIR}/docs/build/ 53 | 54 | # Commit and push the documentation 55 | ( 56 | cd ${TRAVIS_BUILD_DIR}/docs/build 57 | 58 | if [[ `git status --porcelain` ]]; then 59 | echo "... commiting and pushing the docs" 60 | touch . 61 | git add -A . 62 | git commit -m "Rebuild wiki at ${rev}" 63 | git push -q upstream HEAD:master 64 | else 65 | echo "Ignoring deployment. No changes detected." 66 | exit 0 67 | fi 68 | ) 69 | 70 | echo "... deployment ready" 71 | -------------------------------------------------------------------------------- /docs/latest/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting started 3 | type: project 4 | layout: page 5 | project: html-form-validator 6 | version: v1 7 | --- 8 | 9 | As challenged by a [tweet](https://twitter.com/Ocramius/status/680817040429592576), this library extracts validation 10 | rules and filters from a html form and validates submitted user data against it. 11 | 12 | It's pretty crazy what you have to do to get a form build in frameworks. Create a lot of php classes for elements, 13 | validation, etc. So why not build a html form and use the standard element attributes to extract the validation rules 14 | and filters. Together with some powerful html compliant data attributes you can create forms, customize validation 15 | rules and filters in one place. 16 | 17 | ## Installation 18 | 19 | ```bash 20 | $ composer require xtreamwayz/html-form-validator 21 | ``` 22 | 23 | ## How does it work? 24 | 25 | ### 1. Build the form 26 | 27 | Create the form from html. Nothing fancy here. Create the form with the form 28 | factory from a html form and optionally default values. Only the first 29 | `
` element is processed. Following form elements or html code 30 | outside the first form element is ignored. 31 | 32 | ```php 33 | $form = (new FormFactory())->fromHtml($htmlForm, $defaultValues); 34 | ``` 35 | 36 | - The FormFactory creates and returns a Form instance with a new `Laminas\InputFilter\Factory` and 37 | `Laminas\InputFilter\InputFilter`. 38 | - The Form automatically creates default validators and filters for all input elements. 39 | - The Form extracts additional custom validation rules and filters from the form. 40 | - The Form optionally injects default data into the form input elements. 41 | 42 | ### 2. Validate the form 43 | 44 | The easiest way is to use a framework that uses [PSR-7 requests](http://www.php-fig.org/psr/psr-7/). 45 | 46 | ```php 47 | // Validate PSR-7 request and return a ValidationResponseInterface 48 | // It should only start validation if it was a post and if there are submitted values 49 | $validationResult = $form->validateRequest($request); 50 | ``` 51 | 52 | Under the hood it uses [laminas-inputfilter](https://docs.laminas.dev/laminas-inputfilter/) 53 | which makes all its [validators](https://docs.laminas.dev/laminas-validator/set/) and 54 | [filters](https://docs.laminas.dev/laminas-filter/standard-filters/) available to you. 55 | 56 | If you use a framework that doesn't handle PSR-7 requests, you can still reduce boilerplate 57 | code by passing the request method yourself: 58 | 59 | ```php 60 | $validationResult = $form->validate($submittedData, $_SERVER['REQUEST_METHOD']); 61 | ``` 62 | 63 | You can also leave the method validation out. It won't check for a valid `POST` method. 64 | 65 | ```php 66 | $validationResult = $form->validate($submittedData); 67 | ``` 68 | 69 | ### 3. Process validation result 70 | 71 | Submitted data should be valid if it was a post and there are no validation messages set. 72 | 73 | ```php 74 | // It should be valid if it was a post and if there are no validation messages 75 | if ($validationResult->isValid()) { 76 | // Get filter submitted values 77 | $data = $validationResult->getValues(); 78 | 79 | // Process data 80 | 81 | return new RedirectResponse('/'); 82 | } 83 | ``` 84 | 85 | If PSR-7 request methods are not available, you might can check for a valid 86 | post method yourself. 87 | 88 | ```php 89 | if ($_SERVER['REQUEST_METHOD'] == 'POST' && $validationResult->isValid()) { 90 | // ... 91 | } 92 | ``` 93 | 94 | ### 4. Render the form 95 | 96 | Last step is rendering the form and injecting the submitted values and validation messages. 97 | 98 | ```php 99 | // Render the form 100 | return new HtmlResponse($this->template->render('app::edit', [ 101 | 'form' => $form->asString($validationResult), 102 | ])); 103 | ``` 104 | 105 | If you don't want the values and messages injected for you, just leave out the validation result. 106 | 107 | ```php 108 | echo $form->asString(); 109 | ``` 110 | 111 | Before rendering, the FormFactory removes any data validation attributes used to instantiate custom validation 112 | (e.g. `data-validators`, `data-filters`). This also removes possible sensitive data that was used to setup 113 | the validators. 114 | 115 | The `$validationResult` is optional and triggers the following tasks: 116 | 117 | - The FormFactory injects filtered submitted data into the input elements. 118 | - The FormFactory adds error messages next to the input elements. 119 | - The FormFactory sets the `aria-invalid="true"` attribute for invalid input elements. 120 | - The FormFactory adds the bootstrap `has-danger` css class to the parent element. 121 | 122 | ## Submit button detection 123 | 124 | Who doesn't want to know which button is clicked? For this to work the submit button must have a name attribute set. 125 | 126 | ```html 127 |
128 | 129 | 130 |
131 | ``` 132 | 133 | You can check by the button name attribute if a specific button is clicked. 134 | 135 | ```php 136 | // Returns a boolean 137 | $validationResult->isClicked('confirm'); 138 | ``` 139 | 140 | Or get the name of the clicked button. 141 | 142 | ```php 143 | // Returns the name of the clicked button or null if no named was clicked 144 | $validationResult->getClicked(); 145 | ``` 146 | 147 | ## Example 148 | 149 | ```php 150 | // Basic contact form 151 | 152 | $htmlForm = <<<'HTML' 153 |
154 |
155 |
156 |
157 | 158 | 161 |
162 |
163 |
164 |
165 | 166 | 169 |
170 |
171 |
172 | 173 |
174 | 175 | 178 |
179 | 180 |
181 | 182 | 185 |
186 | 187 | 189 | 190 | 191 |
192 | HTML; 193 | 194 | // Create form validator from a twig rendered form template 195 | $form = (new FormFactory())->fromHtml($template->render($htmlForm, [ 196 | 'csrf-token' => '123456' 197 | ])); 198 | 199 | $_POST['name'] = 'Barney Stinsons'; 200 | $_POST['email'] = 'barney.stinsons@example.com'; 201 | $_POST['subject'] = 'Hi'; 202 | $_POST['body'] = 'It is going to be Legen-Wait For It... DARY! LEGENDARY!'; 203 | 204 | // Validate form and return form validation result object 205 | $result = $form->validate($_POST); 206 | 207 | // Check validation result 208 | if ($result->isValid()) { 209 | $data = $result->getValues(); 210 | // Process data ... 211 | } else { 212 | // Inject error messages and filtered values from the result object 213 | echo $form->asString($result); 214 | } 215 | ``` 216 | -------------------------------------------------------------------------------- /docs/latest/api-attributes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API attributes 3 | type: project 4 | layout: page 5 | project: html-form-validator 6 | version: v1 7 | --- 8 | 9 | ## Special Attributes 10 | 11 | ### name and data-input-name 12 | 13 | The name is required to link validation messages and request data. 14 | 15 | ```html 16 | 17 |
18 | ``` 19 | 20 | ### data-reuse-submitted-value 21 | 22 | Reuse the submitted value and inject it as a value. 23 | 24 | ```html 25 | 31 | ``` 32 | 33 | ### data-filters 34 | 35 | Apply filters to the submitted value. Multiple 36 | [standard filters](https://docs.laminas.dev/laminas-filter/standard-filters/) 37 | can be used, separated by a vertical bar. Options can be set with `{key:value,min:2,max:140}`. 38 | The attribute will be removed before rendering the form, including any sensitive options. 39 | 40 | ```html 41 | 42 | ``` 43 | 44 | ### data-validators 45 | 46 | Add extra validators. Multiple 47 | [standard validators](https://docs.laminas.dev/laminas-validator/standard-filters/) 48 | can be used, separated by a vertical bar. Options can be set with `{key:value,min:2,max:140}`. 49 | The attribute will be removed before rendering the form, including any sensitive options. 50 | 51 | ```html 52 | 58 | ``` 59 | 60 | ### checked 61 | 62 | The checked content attribute is a boolean attribute that gives the default checkedness of the input element. When the 63 | checked content attribute is added, if the control does not have dirty checkedness, the user agent must set the 64 | checkedness of the element to true; when the checked content attribute is removed, if the control does not have dirty 65 | checkedness, the user agent must set the checkedness of the element to false. 66 | 67 | ### disabled 68 | 69 | The disabled content attribute is a boolean attribute. 70 | 71 | Constraint validation: If an element is disabled, it is barred from constraint validation. 72 | 73 | ### list 74 | 75 | The list attribute is used to identify an element that lists predefined options suggested to the user. 76 | 77 | If present, its value must be the ID of a datalist element in the same document. 78 | 79 | The **suggestions source element** is the first element in the document in tree order to have an ID equal to the value 80 | of the list attribute, if that element is a datalist element. If there is no list attribute, or if there is no element 81 | with that ID, or if the first element with that ID is not a datalist element, then there is no suggestions source 82 | element. 83 | 84 | ```html 85 | 86 | 87 | 93 | ``` 94 | 95 | ### max 96 | 97 | If the element has a max attribute, and the result of applying the algorithm to convert a string to a number to the 98 | value of the max attribute is a number, then that number is the element's maximum; otherwise, if the type attribute's 99 | current state defines a default maximum, then that is the maximum; otherwise, the element has no maximum. 100 | 101 | ### min 102 | 103 | If the element has a min attribute, and the result of applying the algorithm to convert a string to a number to the 104 | value of the min attribute is a number, then that number is the element's minimum; otherwise, if the type attribute's 105 | current state defines a default minimum, then that is the minimum; otherwise, the element has no minimum. 106 | 107 | The min attribute also defines the step base. 108 | 109 | ### step 110 | 111 | The step attribute indicates the granularity that is expected (and required) of the value or values, by limiting the 112 | allowed values. The section that defines the type attribute's current state also defines the default step, the step 113 | scale factor, and in some cases the default step base, which are used in processing the attribute. 114 | 115 | ### maxlength 116 | 117 | If the input element has a maximum allowed value length, then the code-unit length of the value of the element's 118 | value attribute must be equal to or less than the element's maximum allowed value length. 119 | 120 | ### minlength 121 | 122 | If the input element has a minimum allowed value length, then the code-unit length of the value of the element's 123 | value attribute must be equal to or more than the element's maximum allowed value length. 124 | 125 | ### multiple 126 | 127 | The multiple attribute is a boolean attribute that indicates whether the user is to be allowed to specify more than one 128 | value. 129 | 130 | ### pattern 131 | 132 | The pattern attribute specifies a regular expression against which the control's value, or, when the multiple attribute 133 | applies and is set, the control's values, are to be checked. 134 | 135 | If specified, the attribute's value must match the JavaScript Pattern production. 136 | 137 | If an input element has a pattern attribute specified, and the attribute's value, when compiled as a JavaScript regular 138 | expression with only the "u" flag specified, compiles successfully, then the resulting regular expression is the 139 | element's compiled pattern regular expression. If the element has no such attribute, or if the value doesn't compile 140 | successfully, then the element has no compiled pattern regular expression. [ECMA262] 141 | 142 | ### readonly 143 | 144 | The readonly attribute is a boolean attribute that controls whether or not the user can edit the form control. When 145 | specified, the element is not mutable. 146 | 147 | ### required 148 | 149 | ### aria-required 150 | 151 | The required attribute is a boolean attribute. When specified, the element is required. 152 | 153 | The required attribute triggers the not empty validation. 154 | 155 | ```html 156 | 157 | 158 | 159 | ``` 160 | -------------------------------------------------------------------------------- /docs/latest/api-form-elements.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API form elements 3 | type: project 4 | layout: page 5 | project: html-form-validator 6 | version: v1 7 | --- 8 | 9 | The form validator detects HTML5 form elements and adds default validators depending on the used attributes. 10 | Standard validation rules are added for you so you don't need to repeat those over and over again. And then 11 | there are the special attributes with trigger standard validation: 12 | - [[max|API Attributes#max]] 13 | - [[min|API Attributes#min]] 14 | - [[step|API Attributes#step]] 15 | - [[maxlength|API Attributes#maxlength]] 16 | - [[minlength|API Attributes#minlength]] 17 | - [[multiple|API Attributes#multiple]] 18 | - [[pattern|API Attributes#pattern]] 19 | - [[required|API Attributes#required]], [[aria-required|API Attributes#aria-required]] 20 | 21 | And if you need more validation or specific filters there is a [[data-filters|API Attributes#data-filters]] and 22 | [[data-validators|API Attributes#data-validators]] attribute. 23 | 24 | A full blown text input might look like: 25 | 26 | ```html 27 | 33 | ``` 34 | 35 | ## The input element 36 | 37 | ### Hidden state (type=hidden) 38 | 39 | *Attributes:* 40 | [[value|API Attributes#value]]. 41 | 42 | ```html 43 | 44 | ``` 45 | 46 | Resources: 47 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#hidden-state-(type=hidden)) 48 | 49 | ### Text state (type=text) and Search state (type=search) 50 | 51 | The difference between the Text state and the Search state is primarily stylistic. 52 | 53 | *Attributes:* 54 | [[list|API Attributes#list]], 55 | [[maxlength|API Attributes#maxlength]], 56 | [[minlength|API Attributes#minlength]], 57 | [[pattern|API Attributes#pattern]], 58 | [[disabled|API Attributes#disabled]], 59 | [[readonly|API Attributes#readonly]], 60 | [[required|API Attributes#required]], 61 | [[value|API Attributes#value]]. 62 | 63 | *Filters:* Strip line breaks from the value. 64 | 65 | ```html 66 | 67 | ``` 68 | 69 | Resources: 70 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#text-(type=text)-state-and-search-state-(type=search)) 71 | 72 | ### Telephone state (type=tel) 73 | 74 | *Attributes:* 75 | [[list|API Attributes#list]], 76 | [[maxlength|API Attributes#maxlength]], 77 | [[minlength|API Attributes#minlength]], 78 | [[pattern|API Attributes#pattern]], 79 | [[disabled|API Attributes#disabled]], 80 | [[readonly|API Attributes#readonly]], 81 | [[required|API Attributes#required]], 82 | [[value|API Attributes#value]]. 83 | 84 | *Filters:* Strip line breaks from the value. 85 | 86 | If the `data-validator-country` attribute is set the `PhoneNumberValidator` is used. If not, it falls back to a 87 | very loose regex pattern if none is set: `^\+[0-9]{1,3}[0-9\s]{4,17}$`. 88 | 89 | ```html 90 | 91 | ``` 92 | 93 | Resources: 94 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#telephone-state-(type=tel)) 95 | 96 | ### URL state (type=url) 97 | 98 | *Attributes:* 99 | [[list|API Attributes#list]], 100 | [[maxlength|API Attributes#maxlength]], 101 | [[minlength|API Attributes#minlength]], 102 | [[pattern|API Attributes#pattern]], 103 | [[disabled|API Attributes#disabled]], 104 | [[readonly|API Attributes#readonly]], 105 | [[required|API Attributes#required]], 106 | [[value|API Attributes#value]]. 107 | 108 | *Filters:* Strip line breaks from the value. 109 | 110 | ```html 111 | 112 | 113 | 120 | ``` 121 | 122 | Resources: 123 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#url-state-(type=url)) 124 | 125 | ### E-mail state (type=email) 126 | 127 | *Attributes:* 128 | [[list|API Attributes#list]], 129 | [[maxlength|API Attributes#maxlength]], 130 | [[minlength|API Attributes#minlength]], 131 | [[multiple|API Attributes#multiple]], 132 | [[pattern|API Attributes#pattern]], 133 | [[disabled|API Attributes#disabled]], 134 | [[readonly|API Attributes#readonly]], 135 | [[required|API Attributes#required]], 136 | [[value|API Attributes#value]]. 137 | 138 | *Filters:* Strip line breaks from the value, then strip leading and trailing whitespace from the value. 139 | 140 | ```html 141 | 142 | ``` 143 | 144 | Resources: 145 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#e-mail-state-(type=email)) 146 | 147 | ### Password state (type=password) 148 | 149 | *Attributes:* 150 | [[maxlength|API Attributes#maxlength]], 151 | [[minlength|API Attributes#minlength]], 152 | [[pattern|API Attributes#pattern]], 153 | [[disabled|API Attributes#disabled]], 154 | [[readonly|API Attributes#readonly]], 155 | [[required|API Attributes#required]], 156 | [[value|API Attributes#value]]. 157 | 158 | *Filters:* Strip line breaks from the value. 159 | 160 | ```html 161 | 162 | ``` 163 | 164 | Resources: 165 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#password-state-(type=password)) 166 | 167 | ### Date state (type=date) 168 | 169 | *Attributes:* 170 | [[list|API Attributes#list]], 171 | [[max|API Attributes#max]], 172 | [[min|API Attributes#min]], 173 | [[step|API Attributes#step]], 174 | [[disabled|API Attributes#disabled]], 175 | [[readonly|API Attributes#readonly]], 176 | [[required|API Attributes#required]], 177 | [[value|API Attributes#value]]. 178 | 179 | ```html 180 | 181 | ``` 182 | 183 | Resources: 184 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#date-state-(type=date)) 185 | 186 | ### Month state (type=month) 187 | 188 | *Attributes:* 189 | [[list|API Attributes#list]], 190 | [[max|API Attributes#max]], 191 | [[min|API Attributes#min]], 192 | [[step|API Attributes#step]], 193 | [[disabled|API Attributes#disabled]], 194 | [[readonly|API Attributes#readonly]], 195 | [[required|API Attributes#required]], 196 | [[value|API Attributes#value]]. 197 | 198 | ```html 199 | 200 | ``` 201 | 202 | Resources: 203 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#month-state-(type=month)) 204 | 205 | ### Week state (type=week) 206 | 207 | *Attributes:* 208 | [[list|API Attributes#list]], 209 | [[max|API Attributes#max]], 210 | [[min|API Attributes#min]], 211 | [[step|API Attributes#step]], 212 | [[disabled|API Attributes#disabled]], 213 | [[readonly|API Attributes#readonly]], 214 | [[required|API Attributes#required]], 215 | [[value|API Attributes#value]]. 216 | 217 | ```html 218 | 219 | ``` 220 | 221 | Resources: 222 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#week-state-(type=week)) 223 | 224 | ### Time state (type=time) 225 | 226 | *Attributes:* 227 | [[list|API Attributes#list]], 228 | [[max|API Attributes#max]], 229 | [[min|API Attributes#min]], 230 | [[step|API Attributes#step]], 231 | [[disabled|API Attributes#disabled]], 232 | [[readonly|API Attributes#readonly]], 233 | [[required|API Attributes#required]], 234 | [[value|API Attributes#value]]. 235 | 236 | ```html 237 | 238 | ``` 239 | 240 | Resources: 241 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#time-state-(type=time)) 242 | 243 | ### Local Date and Time state (type=datetime-local) 244 | 245 | *Attributes:* 246 | [[list|API Attributes#list]], 247 | [[max|API Attributes#max]], 248 | [[min|API Attributes#min]], 249 | [[step|API Attributes#step]], 250 | [[disabled|API Attributes#disabled]], 251 | [[readonly|API Attributes#readonly]], 252 | [[required|API Attributes#required]], 253 | [[value|API Attributes#value]]. 254 | 255 | ```html 256 |
257 | Destination 258 |

259 | 260 | 261 |

262 |

263 | 264 | 265 |

266 |
267 | 268 | 274 | ``` 275 | 276 | Resources: 277 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#local-date-and-time-state-(type=datetime-local)) 278 | 279 | ### Number state (type=number) 280 | 281 | *Attributes:* 282 | [[list|API Attributes#list]], 283 | [[max|API Attributes#max]], 284 | [[min|API Attributes#min]], 285 | [[step|API Attributes#step]], 286 | [[disabled|API Attributes#disabled]], 287 | [[readonly|API Attributes#readonly]], 288 | [[required|API Attributes#required]], 289 | [[value|API Attributes#value]]. 290 | 291 | The default step is 1, allowing only integers to be selected by the user. Unless the step base has a non-integer 292 | (float) value in which case floats are allowed. 293 | The base value of step equals the min value if it exists, otherwise it is set to `0`. 294 | To disable the step validation use `step="any"`. 295 | 296 | ```html 297 | 298 | 299 | ``` 300 | 301 | Resources: 302 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#number-state) 303 | 304 | ### Range state (type=range) 305 | 306 | *Attributes:* 307 | [[list|API Attributes#list]], 308 | [[max|API Attributes#max]], 309 | [[min|API Attributes#min]], 310 | [[step|API Attributes#step]], 311 | [[multiple|API Attributes#multiple]], 312 | [[disabled|API Attributes#disabled]], 313 | [[readonly|API Attributes#readonly]], 314 | [[required|API Attributes#required]], 315 | [[value|API Attributes#value]]. 316 | 317 | ```html 318 |
319 |
320 | Outbound flight time 321 | 325 |

326 | 00:0024:00 327 |

328 | 330 |
331 | ... 332 |
333 | ``` 334 | 335 | Resources: 336 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#range-state-(type=range)) 337 | 338 | ### Colour state (type=color) 339 | 340 | *Attributes:* 341 | [[list|API Attributes#list]], 342 | [[disabled|API Attributes#disabled]], 343 | [[value|API Attributes#value]]. 344 | 345 | *Filters:* Convert the value to lowercase. 346 | 347 | ```html 348 | 349 | ``` 350 | 351 | Resources: 352 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#color-state-(type=color)) 353 | 354 | ### Checkbox state (type=checkbox) 355 | 356 | *Attributes:* 357 | [[checked|API Attributes#checked]], 358 | [[required|API Attributes#required]], 359 | [[value|API Attributes#value]]. 360 | 361 | ```html 362 | 363 | ``` 364 | 365 | Input arrays are supported. The following will return an array `['cars'=>['audi', 'bmw']]`. 366 | 367 | ```html 368 |
369 | 370 | 371 | 372 |
373 | ``` 374 | 375 | Resources: 376 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#checkbox-state-(type=checkbox)) 377 | 378 | ### Radio Button state (type=radio) 379 | 380 | *Attributes:* 381 | [[checked|API Attributes#checked]], 382 | [[required|API Attributes#required]], 383 | [[value|API Attributes#value]]. 384 | 385 | ```html 386 | Male
387 | Female
388 | Other 389 | ``` 390 | 391 | Resources: 392 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#radio-button-state-(type=radio)) 393 | 394 | ### File Upload state (type=file) 395 | 396 | *Attributes:* 397 | [[accept|API Attributes#accept]], 398 | [[multiple|API Attributes#multiple]], 399 | [[required|API Attributes#required]], 400 | [[value|API Attributes#value]]. 401 | 402 | ```html 403 | 404 | ``` 405 | 406 | Resources: 407 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#file-upload-state-(type=file)) 408 | 409 | ### Submit Button state (type=submit) 410 | 411 | *Attributes:* 412 | [[value|API Attributes#value]]. 413 | 414 | ```html 415 | 416 | ``` 417 | 418 | Resources: 419 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#submit-button-state-(type=submit)) 420 | 421 | ### Image Button state (type=image) 422 | 423 | *Attributes:* 424 | [[value|API Attributes#value]]. 425 | 426 | ```html 427 |
428 | 429 |
430 | ``` 431 | 432 | Resources: 433 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#image-button-state-(type=image)) 434 | 435 | ### Button state (type=button) 436 | 437 | *Attributes:* 438 | [[value|API Attributes#value]]. 439 | 440 | ```html 441 | 442 | ``` 443 | 444 | Resources: 445 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#button-state-(type=button)) 446 | 447 | ## The button element 448 | 449 | *Attributes:* 450 | [[value|API Attributes#value]]. 451 | 452 | ```html 453 | 454 | ``` 455 | 456 | Resources: 457 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#the-button-element) 458 | 459 | ## The select element 460 | 461 | *Attributes:* 462 | [[multiple|API Attributes#multiple]], 463 | [[disabled|API Attributes#disabled]], 464 | [[readonly|API Attributes#readonly]], 465 | [[required|API Attributes#required]], 466 | [[value|API Attributes#value]]. 467 | 468 | ```html 469 | 475 | ``` 476 | 477 | Resources: 478 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#the-select-element) 479 | 480 | ## The textarea element 481 | 482 | *Attributes:* 483 | [[maxlength|API Attributes#maxlength]], 484 | [[minlength|API Attributes#minlength]], 485 | [[disabled|API Attributes#disabled]], 486 | [[readonly|API Attributes#readonly]], 487 | [[required|API Attributes#required]]. 488 | 489 | ```html 490 | 491 | ``` 492 | 493 | Resources: 494 | [whatwg](https://html.spec.whatwg.org/multipage/forms.html#the-textarea-element) 495 | -------------------------------------------------------------------------------- /docs/latest/customizing-the-inputfilter.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Customizing the InputFilter 3 | type: project 4 | layout: page 5 | project: html-form-validator 6 | version: v1 7 | --- 8 | 9 | ## Setting a default InputFilter Factory 10 | 11 | Sometimes you need custom filters or validators. To register those, a `Laminas\InputFilter\Factory` can be used and 12 | injected into the FormFactory. Or use the included `InputFilterFactory` to set this up for you from this config: 13 | 14 | ```php 15 | return = [ 16 | 'laminas-inputfilter' => [ 17 | 'validators' => [ 18 | // Attach custom validators or override standard validators 19 | 'invokables' => [ 20 | 'recaptcha' => RecaptchaValidator::class, 21 | ], 22 | ], 23 | 'filters' => [ 24 | // Attach custom filters or override standard filters 25 | 'invokables' => [], 26 | ], 27 | ], 28 | ]; 29 | ``` 30 | 31 | ## Re-usable InputFilters 32 | 33 | Still want to use a html form instead of generating it with complicated classes, but you want to reuse the validation 34 | part? We got you covered. The `FormFactory` and `Form` accepts `Laminas\InputFilter\InputFilterInterface`s so they can be 35 | re-used everywhere in your app. The `Form` only creates new filters and validators for named input elements that do not 36 | exist yet in the injected InputFilter. 37 | 38 | ```php 39 | $form = (new FormFactory())->fromHtml($htmlForm, $defaultValues, $userInputFilter); 40 | ``` 41 | 42 | ## Custom Validators and Filters 43 | 44 | Setting up custom validators and filters is a bit more work but it isn't complicated. Instead of creating the 45 | FormFactory with its static `fromHtml` method, the constructor is needed with a configured `Laminas\InputFilter\Factory` 46 | and a `Psr\Container\ContainerInterface`. 47 | 48 | ```php 49 | $config = [ 50 | 'laminas-inputfilter' => [ 51 | 'validators' => [ 52 | // Attach custom validators or override standard validators 53 | 'invokables' => [ 54 | 'recaptcha' => Xtreamwayz\HTMLFormValidator\Validator\RecaptchaValidator::class, 55 | ], 56 | ], 57 | 'filters' => [ 58 | // Attach custom filters or override standard filters 59 | 'invokables' => [ 60 | ], 61 | ], 62 | ], 63 | ]; 64 | 65 | // Create a container-interop compatible container with the custom validator configuration 66 | $container = new Laminas\ServiceManager\ServiceManager($dependencies); 67 | $container->setService('config', $config); 68 | 69 | // Use the InputFilterFactory to do all the work for you 70 | $factory = new Xtreamwayz\HTMLFormValidator\InputFilterFactory(); 71 | $inputFilterFactory = $factory($container); 72 | 73 | // Load the html form into the FormFactory 74 | $formFactory = new FormFactory($inputFilterFactory); 75 | 76 | // Create a form instance 77 | $form = $formFactory->fromHtml($html); 78 | 79 | // Validate the form 80 | $result = $form->validate($_POST); 81 | 82 | // Display the plain form 83 | echo $form->asString(); 84 | 85 | // Display the form with the submitted values and validation messages 86 | echo $form->asString($validationResult); 87 | ``` 88 | -------------------------------------------------------------------------------- /docs/latest/example-expressive-action.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Example Expressive action 3 | type: project 4 | layout: page 5 | project: html-form-validator 6 | version: v1 7 | --- 8 | 9 | Let's take the contact form as an example and use it in a laminas-expressive application with Twig as a renderer. The 10 | form is pretty basic and has extra StringTrim and StripTags filters for the name and subject input fields. It also 11 | has csrf protection with a hidden token which is validated with the identical validator. 12 | 13 | ```html 14 | 15 |
16 |
17 | 18 | 28 |
29 | 30 |
31 | 32 | 41 |
42 | 43 |
44 | 45 | 55 |
56 | 57 |
58 | 59 | 68 |
69 | 70 | 71 | 78 | 79 | 80 |
81 | ``` 82 | 83 | This is an example of how an Action may look like. We'll walk through each step in the comments. 84 | 85 | ```php 86 | template = $template; 106 | } 107 | 108 | /** 109 | * @param ServerRequestInterface $request 110 | * @param ResponseInterface $response 111 | * @param callable|null $next 112 | * 113 | * @return HtmlResponse 114 | */ 115 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null) 116 | { 117 | // Use [PSR7Session](https://github.com/Ocramius/PSR7Session) to store the session data. 118 | 119 | /* @var \PSR7Session\Session\SessionInterface $session */ 120 | $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); 121 | 122 | // Generate csrf token if needed 123 | if (!$session->get('csrf')) { 124 | $session->set('csrf', md5(random_bytes(32))); 125 | } 126 | 127 | // Generate the form from the template with the template renderer and inject the csrf token 128 | $form = (new FormFactory())->fromHtml($this->template->render('app::contact-form', [ 129 | 'token' => $session->get('csrf'), 130 | ])); 131 | 132 | // Validate PSR-7 request and get a validation result 133 | $validationResult = $form->validateRequest($request); 134 | 135 | // It should be valid if it was a post and if there are no validation messages 136 | if ($validationResult->isValid()) { 137 | // Get filtered submitted values 138 | $data = $validationResult->getValues(); 139 | 140 | // Process data 141 | 142 | return new RedirectResponse('/'); 143 | } 144 | 145 | // Display the form and inject the validation messages if there are any 146 | return new HtmlResponse($this->template->render('app::edit', [ 147 | 'form' => $form->asString($validationResult), 148 | ])); 149 | } 150 | } 151 | ``` 152 | -------------------------------------------------------------------------------- /docs/latest/example-expressive-custom-validators.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Example Expressive custom validators 3 | type: project 4 | layout: page 5 | project: html-form-validator 6 | version: v1 7 | --- 8 | 9 | Let's take the contact form as an example and use it in a laminas-expressive application with Twig as a renderer. The 10 | form is pretty basic and has extra StringTrim and StripTags filters for the name and subject input fields. It also 11 | has csrf protection with a hidden token which is validated with the identical validator. And use a custom recaptcha 12 | validator for bot protection. 13 | 14 | ```html 15 | 16 |
17 |
18 | 19 | 29 |
30 | 31 |
32 | 33 | 42 |
43 | 44 |
45 | 46 | 56 |
57 | 58 |
59 | 60 | 69 |
70 | 71 |
72 |
80 |
81 | 82 | 89 | 90 | 91 |
92 | ``` 93 | 94 | This is an example of how an Action may look like. The InputFilterFactory is injected which gives the FormFactory 95 | access to the custom recaptcha validator. 96 | 97 | ```php 98 | template = $template; 122 | $this->formFactory = $formFactory; 123 | } 124 | 125 | /** 126 | * @param ServerRequestInterface $request 127 | * @param ResponseInterface $response 128 | * @param callable|null $next 129 | * 130 | * @return HtmlResponse 131 | */ 132 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null) 133 | { 134 | // Use [PSR7Session](https://github.com/Ocramius/PSR7Session) to store the session data. 135 | 136 | /* @var \PSR7Session\Session\SessionInterface $session */ 137 | $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); 138 | 139 | // Generate csrf token 140 | if (!$session->get('csrf')) { 141 | $session->set('csrf', md5(random_bytes(32))); 142 | } 143 | 144 | // Build the form validation from the template with the template renderer and inject the csrf token. Since the 145 | // formFactory is used with a custom InputFilterFactory, you have access to the custom recaptcha validator. 146 | $form = $this->formFactory->fromHtml($this->template->render('app::form', [ 147 | 'token' => $session->get('csrf'), 148 | ])); 149 | 150 | // Validate PSR-7 request and get a validation result 151 | $validationResult = $form->validateRequest($request); 152 | 153 | // It should be valid if it was a post and if there are no validation messages 154 | if ($validationResult->isValid()) { 155 | // Get filtered submitted values 156 | $data = $validationResult->getValues(); 157 | 158 | // Process data 159 | 160 | return new RedirectResponse('/'); 161 | } 162 | 163 | // Display the form and inject the validation messages if there are any 164 | return new HtmlResponse($this->template->render('app::edit', [ 165 | 'form' => $form->asString($validationResult), 166 | ])); 167 | } 168 | } 169 | ``` 170 | 171 | To register the custom validator it needs to be added to the configuration. 172 | 173 | ```php 174 | [ 181 | 'invokables' => [ 182 | ], 183 | 'factories' => [ 184 | // Use the InputFilterFactory helper to configure the InputFactory 185 | Laminas\InputFilter\Factory::class => Xtreamwayz\HTMLFormValidator\InputFilterFactory::class, 186 | ], 187 | ], 188 | */ 189 | 190 | 'laminas-inputfilter' => [ 191 | 'validators' => [ 192 | // Attach custom validators or override standard validators 193 | 'invokables' => [ 194 | 'recaptcha' => Xtreamwayz\HTMLFormValidator\Validator\RecaptchaValidator::class, 195 | ], 196 | ], 197 | 'filters' => [ 198 | // Attach custom filters or override standard filters 199 | 'invokables' => [ 200 | ], 201 | ], 202 | ], 203 | ]; 204 | ``` 205 | -------------------------------------------------------------------------------- /docs/latest/example-password-confirmation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Example password confirmation 3 | type: project 4 | layout: page 5 | project: html-form-validator 6 | version: v1 7 | --- 8 | 9 | ```html 10 |
11 | 12 | 13 | 14 |
15 | ``` 16 | -------------------------------------------------------------------------------- /docs/latest/example-recaptcha.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Example Recaptcha 3 | type: project 4 | layout: page 5 | project: html-form-validator 6 | version: v1 7 | --- 8 | 9 | Sometimes you need to validate javascript generated from fields. This is easy with the `data-input-name` and 10 | `data-validators` attributes. 11 | 12 | ```html 13 |
14 |
18 | 19 | 20 |
21 | ``` 22 | 23 | The `data-sitekey` and `data-theme` are recaptcha settings. The `data-input-name` attribute enables the input filter 24 | for `g-recatcha-response` and the `data-validators` attribute enables the validation. 25 | 26 | If you are wondering, the recaptcha keys will be injected into the form by the template renderer. The pub_key is 27 | needed for recaptcha to function and will be send to the user with the form. The priv_key is for server site 28 | validation and will be passed to the recaptcha validator. Before rendering the form, the secret priv_key will be 29 | removed as the `data-validators` attribute will automatically removed for along with other attributes needed for 30 | configuring the FormFactory only. 31 | -------------------------------------------------------------------------------- /docs/latest/example-symfony-action.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Example Symfony action 3 | type: project 4 | layout: page 5 | project: html-form-validator 6 | version: v1 7 | --- 8 | 9 | Symfony forms can be a real pain to design and even worse to debug. Luckily you are not bound to use Symfony forms and 10 | can use something else if you like to. For example this html-form-factory :) 11 | 12 | Let's take the contact form as an example and use it in a Symfony application. For the fun of it the ADR 13 | (Action-Domain-Response) pattern is being to used which can easily be achieved by registering the action as a service. 14 | Or use the [DunglasActionBundle](https://github.com/dunglas/DunglasActionBundle/) to do this for you. 15 | 16 | The form is pretty basic and has extra StringTrim and StripTags filters for the name and subject input fields. It 17 | also has csrf protection with a hidden token which is validated with the identical validator. 18 | 19 | ```html 20 | 21 |
22 |
23 | 24 | 27 |
28 | 29 |
30 | 31 | 34 |
35 | 36 |
37 | 38 | 41 |
42 | 43 |
44 | 45 | 48 |
49 | 50 | 51 | 52 | 53 | 54 |
55 | ``` 56 | 57 | This is an example of how an Action may look like. We'll walk through each step in the comments. 58 | 59 | ```php 60 | router = $router; 83 | $this->template = $template; 84 | } 85 | 86 | /** 87 | * @Route("/contact", name="contact") 88 | * @Method({"GET", "POST"}) 89 | */ 90 | public function __invoke(Request $request) 91 | { 92 | // Generate csrf token if needed 93 | $session = $request->getSession(); 94 | if (!$session->get('csrf')) { 95 | $session->set('csrf', md5(random_bytes(32))); 96 | } 97 | 98 | // Generate the form from the template with the template renderer and inject the csrf token 99 | $form = (new FormFactory())->fromHtml($this->template->render('contact-form.html.twig', [ 100 | 'token' => $session->get('csrf'), 101 | ])); 102 | 103 | // Validate request and get a validation result. The request method is passed to check if the form is 104 | // being posted. 105 | $validationResult = $form->validate($request->request->all(), $request->getMethod()); 106 | 107 | // It should be valid if it was a post and if there are no validation messages 108 | if ($validationResult->isValid()) { 109 | // Get filtered submitted values 110 | $data = $validationResult->getValues(); 111 | 112 | // Process data 113 | 114 | return new RedirectResponse($this->router->generate('homepage')); 115 | } 116 | 117 | // Display the form and inject the validation messages if there are any 118 | return new HtmlResponse($this->template->render('app::edit', [ 119 | 'form' => $form->asString($validationResult), 120 | ])); 121 | } 122 | } 123 | ``` 124 | -------------------------------------------------------------------------------- /docs/latest/known-issues.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Known issues 3 | type: project 4 | layout: page 5 | project: html-form-validator 6 | version: v1 7 | --- 8 | 9 | ## The form doesn't render all elements 10 | 11 | Check if you use a valid form. A valid form includes the form tag. 12 | 13 | ```html 14 |
15 | 16 | 17 |
18 | ``` 19 | 20 | Renders as: 21 | 22 | ```html 23 |
24 | 25 | 26 |
27 | ``` 28 | 29 | And a form with a missing form tag: 30 | 31 | ```html 32 | 33 | 34 | ``` 35 | 36 | Renders as: 37 | 38 | ```html 39 | 40 | ``` 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xtreamwayz/html-form-validator 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | src 15 | test 16 | vendor 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | test 11 | 12 | 13 | 14 | 15 | 16 | src 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>xtreamwayz/.github:renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | $this->getDependencies(), 15 | 'html-form-validator' => $this->getOptions(), 16 | ]; 17 | } 18 | 19 | public function getDependencies(): array 20 | { 21 | return [ 22 | 'factories' => [ 23 | Factory::class => InputFilterFactory::class, 24 | FormFactory::class => FormFactoryFactory::class, 25 | ], 26 | ]; 27 | } 28 | 29 | public function getOptions(): array 30 | { 31 | return [ 32 | 'cssHasErrorClass' => 'has-validation-error', 33 | 'cssErrorClass' => 'has-validation-error', 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Form.php: -------------------------------------------------------------------------------- 1 | |FormElement\BaseFormElement[] */ 58 | private $formElements = [ 59 | 'checkbox' => FormElement\Checkbox::class, 60 | 'color' => FormElement\Color::class, 61 | 'date' => FormElement\Date::class, 62 | 'datetime-local' => FormElement\DateTime::class, 63 | 'email' => FormElement\Email::class, 64 | 'file' => FormElement\File::class, 65 | 'hidden' => FormElement\Hidden::class, 66 | 'month' => FormElement\Month::class, 67 | 'number' => FormElement\Number::class, 68 | 'password' => FormElement\Password::class, 69 | 'radio' => FormElement\Radio::class, 70 | 'range' => FormElement\Range::class, 71 | 'search' => FormElement\Text::class, 72 | 'select' => FormElement\Select::class, 73 | 'tel' => FormElement\Tel::class, 74 | 'text' => FormElement\Text::class, 75 | 'textarea' => FormElement\Textarea::class, 76 | 'time' => FormElement\Time::class, 77 | 'url' => FormElement\Url::class, 78 | 'week' => FormElement\Week::class, 79 | ]; 80 | 81 | /** 82 | * @inheritDoc 83 | */ 84 | public function __construct( 85 | DOMDocument $document, 86 | array $defaultValues, 87 | ?Factory $factory, 88 | ?InputFilterInterface $inputFilter, 89 | ?array $options = null 90 | ) { 91 | $this->document = $document; 92 | $this->factory = $factory ?? new Factory(); 93 | $this->inputFilter = $inputFilter ?? $this->factory->createInputFilter([]); 94 | 95 | $options = $options ?? []; 96 | $this->cssHasErrorClass = $options['cssHasErrorClass'] ?? 'has-validation-error'; 97 | $this->cssErrorClass = $options['cssErrorClass'] ?? 'validation-error'; 98 | 99 | $this->setData($defaultValues, true); 100 | } 101 | 102 | /** 103 | * @inheritDoc 104 | */ 105 | public function asString(?ValidationResultInterface $result = null): string 106 | { 107 | if ($result) { 108 | // Inject data if a result is set 109 | $this->setData($result->getValues()); 110 | $this->setMessages($result->getMessages()); 111 | } 112 | 113 | // Always remove form validator specific attributes before rendering the form 114 | // to clean it up and remove possible sensitive data 115 | /** @var DOMElement $node */ 116 | foreach ($this->getNodeList() as $name => $node) { 117 | $node->removeAttribute('data-reuse-submitted-value'); 118 | $node->removeAttribute('data-input-name'); 119 | $node->removeAttribute('data-validators'); 120 | $node->removeAttribute('data-filters'); 121 | } 122 | 123 | $this->document->formatOutput = true; 124 | 125 | // Return the first form only to prevent returning the XML declaration 126 | return $this->document->saveHTML($this->document->getElementsByTagName('form')->item(0)); 127 | } 128 | 129 | /** 130 | * @inheritDoc 131 | */ 132 | public function validateRequest(ServerRequestInterface $request): ValidationResultInterface 133 | { 134 | return $this->validate((array) $request->getParsedBody(), $request->getMethod()); 135 | } 136 | 137 | /** 138 | * @inheritDoc 139 | */ 140 | public function validate(array $data, ?string $method = null): ValidationResultInterface 141 | { 142 | if ($method !== null && $method !== 'POST') { 143 | // Not a post request, skip validation 144 | return new ValidationResult([], [], [], $method); 145 | } 146 | 147 | $inputFilter = $this->inputFilter ?? $this->factory->createInputFilter([]); 148 | 149 | // Add all validators and filters to the InputFilter 150 | $this->buildInputFilterFromForm($inputFilter); 151 | 152 | $inputFilter->setData($data); 153 | $messages = []; 154 | 155 | // Do some validation 156 | if (! $inputFilter->isValid()) { 157 | foreach ($inputFilter->getInvalidInput() as $message) { 158 | $messages[$message->getName()] = $message->getMessages(); 159 | } 160 | } 161 | 162 | // Get the submit button 163 | $submitName = null; 164 | foreach ($this->getSubmitStateNodeList() as $name) { 165 | if (! array_key_exists($name, $data)) { 166 | continue; 167 | } 168 | 169 | $submitName = $name; 170 | } 171 | 172 | // Return validation result 173 | return new ValidationResult( 174 | $inputFilter->getRawValues(), 175 | $inputFilter->getValues(), 176 | $messages, 177 | $method, 178 | $submitName 179 | ); 180 | } 181 | 182 | /** 183 | * Build the InputFilter, validators and filters from form fields 184 | */ 185 | private function buildInputFilterFromForm(InputFilterInterface $inputFilter): void 186 | { 187 | /** @var DOMElement $node */ 188 | foreach ($this->getNodeList() as $name => $node) { 189 | if ($inputFilter->has($name)) { 190 | continue; 191 | } 192 | 193 | // Detect element type 194 | $type = $node->getAttribute('type'); 195 | if ($node->tagName === 'textarea') { 196 | $type = 'textarea'; 197 | } elseif ($node->tagName === 'select') { 198 | $type = 'select'; 199 | } 200 | 201 | if (substr($name, -2) === '[]') { 202 | $input = new ArrayInput(); 203 | $inputFilter->add($input, substr($name, 0, -2)); 204 | continue; 205 | } 206 | 207 | // Add validation 208 | if (array_key_exists($type, $this->formElements)) { 209 | $elementClass = $this->formElements[$type]; 210 | } else { 211 | // Create a default validator 212 | $elementClass = $this->formElements['text']; 213 | } 214 | 215 | /** @var InputProviderInterface $element */ 216 | $element = new $elementClass($node, $this->document); 217 | $input = $this->factory->createInput($element); 218 | $inputFilter->add($input, $name); 219 | } 220 | } 221 | 222 | /** 223 | * Get form elements and create an id if needed 224 | * 225 | * return Generator 226 | */ 227 | private function getNodeList(): Generator 228 | { 229 | $xpath = new DOMXPath($this->document); 230 | $nodeList = $xpath->query('//input | //textarea | //select | //div[@data-input-name]'); 231 | 232 | /** @var DOMElement $node */ 233 | foreach ($nodeList as $node) { 234 | $name = $node->getAttribute('name'); 235 | if (! $name) { 236 | $name = $node->getAttribute('data-input-name'); 237 | } 238 | 239 | if (! $name || $node->getAttribute('type') === 'submit') { 240 | // At least a name is needed to submit a value. 241 | // Silently continue, might be a submit button. 242 | continue; 243 | } 244 | 245 | if ($node->hasAttribute('disabled')) { 246 | // Ignore disabled nodes 247 | continue; 248 | } 249 | 250 | yield $name => $node; 251 | } 252 | } 253 | 254 | /** 255 | * Get names of available named submit elements 256 | */ 257 | private function getSubmitStateNodeList(): Generator 258 | { 259 | $xpath = new DOMXPath($this->document); 260 | $nodeList = $xpath->query('//input[@type="submit"] | //button[@type="submit"]'); 261 | 262 | /** @var DOMElement $node */ 263 | foreach ($nodeList as $node) { 264 | $name = $node->getAttribute('name'); 265 | if (! $name) { 266 | // At least a name is needed to submit a value. 267 | continue; 268 | } 269 | 270 | yield $name; 271 | } 272 | } 273 | 274 | /** 275 | * Set values and element checked and selected states 276 | */ 277 | private function setData(array $data, ?bool $force = null): void 278 | { 279 | $force = $force ?? false; 280 | 281 | /** @var DOMElement $node */ 282 | foreach ($this->getNodeList() as $name => $node) { 283 | $arrayName = null; 284 | $value = null; 285 | 286 | // Strip the array notation from the field name 287 | if (substr($name, -2) === '[]') { 288 | $arrayName = substr($name, 0, -2); 289 | // Check if it has a value set 290 | if (! array_key_exists($arrayName, $data)) { 291 | continue; 292 | } 293 | $value = $data[$arrayName]; 294 | } 295 | 296 | if (array_key_exists($name, $data)) { 297 | $value = $data[$name]; 298 | } 299 | 300 | if ($value === null) { 301 | // No value set for this element 302 | continue; 303 | } 304 | 305 | $reuseSubmittedValue = filter_var( 306 | $node->getAttribute('data-reuse-submitted-value'), 307 | FILTER_VALIDATE_BOOLEAN 308 | ); 309 | 310 | if (! $reuseSubmittedValue && $force === false) { 311 | // Don't need to set the value 312 | continue; 313 | } 314 | 315 | if (in_array($node->getAttribute('type'), ['checkbox', 'radio'], true)) { 316 | if ( 317 | $value === $node->getAttribute('value') 318 | || (is_array($value) && in_array($node->getAttribute('value'), $value, true)) 319 | ) { 320 | $node->setAttribute('checked', 'checked'); 321 | } else { 322 | $node->removeAttribute('checked'); 323 | } 324 | } elseif ($node->nodeName === 'select') { 325 | /** @var DOMElement $option */ 326 | foreach ($node->getElementsByTagName('option') as $option) { 327 | if ($value === $option->getAttribute('value')) { 328 | $option->setAttribute('selected', 'selected'); 329 | } else { 330 | $option->removeAttribute('selected'); 331 | } 332 | } 333 | } elseif ($node->nodeName === 'input') { 334 | // Set value for input elements 335 | $node->setAttribute('value', (string) $value); 336 | } elseif ($node->nodeName === 'textarea') { 337 | $node->nodeValue = $value; 338 | } 339 | } 340 | } 341 | 342 | /** 343 | * Set validation messages, bootstrap style 344 | */ 345 | private function setMessages(array $data): void 346 | { 347 | /** 348 | * @var string $name 349 | * @var array $errors 350 | */ 351 | foreach ($data as $name => $errors) { 352 | // Not sure if this can be optimized and create the DOMXPath only once. 353 | // At this point the dom is constantly changing. 354 | $xpath = new DOMXPath($this->document); 355 | // Get all elements with the name 356 | $nodeList = $xpath->query(sprintf('//*[@name="%1$s"] | //*[@data-input-name="%1$s"]', $name)); 357 | 358 | if ($nodeList->length === 0) { 359 | // No element found for this element ??? 360 | continue; 361 | } 362 | 363 | // Get first element only 364 | $node = $nodeList->item(0); 365 | 366 | /** @var DOMElement $parent */ 367 | $parent = $node->parentNode; 368 | if (strpos($parent->getAttribute('class'), $this->cssHasErrorClass) === false) { 369 | // Set error class to parent 370 | $class = trim($parent->getAttribute('class') . ' ' . $this->cssHasErrorClass); 371 | $parent->setAttribute('class', $class); 372 | } 373 | 374 | // Inject error messages 375 | foreach ($errors as $code => $message) { 376 | $div = $this->document->createElement('div'); 377 | $div->setAttribute('class', $this->cssErrorClass); 378 | $div->nodeValue = $message; 379 | $node->parentNode->insertBefore($div, $node->nextSibling); 380 | } 381 | 382 | /** @var DOMElement $node */ 383 | foreach ($nodeList as $node) { 384 | // Set aria-invalid attribute on all elements 385 | $node->setAttribute('aria-invalid', 'true'); 386 | } 387 | } 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /src/FormElement/BaseFormElement.php: -------------------------------------------------------------------------------- 1 | node = $node; 29 | $this->document = $document; 30 | } 31 | 32 | /** 33 | * Should return an array specification compatible with 34 | * {@link Laminas\InputFilter\Factory::createInput()}. 35 | * 36 | * @return array 37 | */ 38 | public function getInputSpecification(): array 39 | { 40 | $spec = [ 41 | 'name' => $this->getName(), 42 | 'required' => $this->isRequired(), 43 | ]; 44 | 45 | $filters = $this->getFilters(); 46 | 47 | if ($this->node->hasAttribute('data-filters')) { 48 | foreach ($this->parseDataAttribute($this->node->getAttribute('data-filters')) as $name => $options) { 49 | $filters[] = [ 50 | 'name' => $name, 51 | 'options' => $options, 52 | ]; 53 | } 54 | } 55 | 56 | if (! empty($filters)) { 57 | $spec['filters'] = $filters; 58 | } 59 | 60 | $validators = $this->getValidators(); 61 | 62 | if ($this->node->hasAttribute('data-validators')) { 63 | foreach ($this->parseDataAttribute($this->node->getAttribute('data-validators')) as $name => $options) { 64 | $validators[] = [ 65 | 'name' => $name, 66 | 'options' => $options, 67 | ]; 68 | } 69 | } 70 | 71 | if (! empty($validators)) { 72 | $spec['validators'] = $validators; 73 | } 74 | 75 | return $spec; 76 | } 77 | 78 | protected function getName(): string 79 | { 80 | $name = $this->node->getAttribute('name'); 81 | if (! $name) { 82 | $name = $this->node->getAttribute('data-input-name'); 83 | } 84 | 85 | return $name; 86 | } 87 | 88 | protected function isRequired(): bool 89 | { 90 | return $this->node->hasAttribute('required') || $this->node->getAttribute('aria-required') === 'true'; 91 | } 92 | 93 | protected function getFilters(): array 94 | { 95 | return []; 96 | } 97 | 98 | protected function getValidators(): array 99 | { 100 | return []; 101 | } 102 | 103 | /** 104 | * Parse data attribute value for validators, filters and options 105 | */ 106 | protected function parseDataAttribute(string $dataAttribute): Generator 107 | { 108 | $matches = []; 109 | preg_match_all('/([a-zA-Z]+)([^|]*)/', $dataAttribute, $matches, PREG_SET_ORDER); 110 | 111 | foreach ($matches as $match) { 112 | $name = $match[1]; 113 | $options = []; 114 | 115 | if (isset($match[2])) { 116 | $allOptions = explode(',', $match[2]); 117 | foreach ($allOptions as $option) { 118 | $option = explode(':', $option); 119 | if (! isset($option[0], $option[1])) { 120 | continue; 121 | } 122 | 123 | $options[trim($option[0], ' {}\'\"')] = trim($option[1], ' {}\'\"'); 124 | } 125 | } 126 | 127 | yield $name => $options; 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/FormElement/Checkbox.php: -------------------------------------------------------------------------------- 1 | IdenticalValidator::class, 16 | 'options' => [ 17 | 'token' => $this->node->getAttribute('value'), 18 | ], 19 | ], 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/FormElement/Color.php: -------------------------------------------------------------------------------- 1 | StringToLowerFilter::class], 16 | ]; 17 | } 18 | 19 | protected function getValidators(): array 20 | { 21 | return [ 22 | [ 23 | 'name' => RegexValidator::class, 24 | 'options' => ['pattern' => '/^#[0-9a-fA-F]{6}$/'], 25 | ], 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/FormElement/Date.php: -------------------------------------------------------------------------------- 1 | node->getAttribute('step') ?: 1; // Days 23 | $baseValue = $this->node->getAttribute('min') ?: date($this->format, 0); 24 | 25 | return [ 26 | 'name' => DateStepValidator::class, 27 | 'options' => [ 28 | 'format' => $this->format, 29 | 'baseValue' => $baseValue, 30 | 'step' => new DateInterval(sprintf('P%dD', $stepValue)), 31 | 'timezone' => new DateTimeZone('UTC'), 32 | ], 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/FormElement/DateTime.php: -------------------------------------------------------------------------------- 1 | getDateValidator(); 25 | 26 | if ($this->node->hasAttribute('min')) { 27 | $validators[] = [ 28 | 'name' => GreaterThanValidator::class, 29 | 'options' => [ 30 | 'min' => $this->node->getAttribute('min'), 31 | 'inclusive' => true, 32 | ], 33 | ]; 34 | } 35 | 36 | if ($this->node->hasAttribute('max')) { 37 | $validators[] = [ 38 | 'name' => LessThanValidator::class, 39 | 'options' => [ 40 | 'max' => $this->node->getAttribute('max'), 41 | 'inclusive' => true, 42 | ], 43 | ]; 44 | } 45 | 46 | if ( 47 | ! $this->node->hasAttribute('step') 48 | || $this->node->getAttribute('step') !== 'any' 49 | ) { 50 | $validators[] = $this->getStepValidator(); 51 | } 52 | 53 | return $validators; 54 | } 55 | 56 | protected function getDateValidator(): array 57 | { 58 | return [ 59 | 'name' => DateValidator::class, 60 | 'options' => [ 61 | 'format' => $this->format, 62 | ], 63 | ]; 64 | } 65 | 66 | protected function getStepValidator(): array 67 | { 68 | $stepValue = $this->node->getAttribute('step') ?: 1; // Minutes 69 | $baseValue = $this->node->getAttribute('min') ?: date($this->format, 0); 70 | 71 | return [ 72 | 'name' => DateStepValidator::class, 73 | 'options' => [ 74 | 'format' => $this->format, 75 | 'baseValue' => $baseValue, 76 | 'step' => new DateInterval(sprintf('PT%dM', $stepValue)), 77 | ], 78 | ]; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/FormElement/Email.php: -------------------------------------------------------------------------------- 1 | StripNewlinesFilter::class], 25 | ['name' => StringTrimFilter::class], 26 | ]; 27 | } 28 | 29 | protected function getValidators(): array 30 | { 31 | $validators = []; 32 | 33 | if ($this->node->hasAttribute('multiple')) { 34 | $validators[] = [ 35 | 'name' => ExplodeValidator::class, 36 | 'options' => [ 37 | 'validator' => $this->getEmailValidator(), 38 | ], 39 | ]; 40 | } else { 41 | $validators[] = $this->getEmailValidator(); 42 | } 43 | 44 | if ($this->node->hasAttribute('minlength') || $this->node->hasAttribute('maxlength')) { 45 | $validators[] = [ 46 | 'name' => StringLengthValidator::class, 47 | 'options' => [ 48 | 'min' => $this->node->getAttribute('minlength') ?: 0, 49 | 'max' => $this->node->getAttribute('maxlength') ?: null, 50 | ], 51 | ]; 52 | } 53 | 54 | if ($this->node->hasAttribute('pattern')) { 55 | $validators[] = [ 56 | 'name' => RegexValidator::class, 57 | 'options' => [ 58 | 'pattern' => sprintf('/%s/', $this->node->getAttribute('pattern')), 59 | ], 60 | ]; 61 | } 62 | 63 | return $validators; 64 | } 65 | 66 | protected function getEmailValidator(): array 67 | { 68 | return [ 69 | 'name' => EmailAddressValidator::class, 70 | 'options' => [ 71 | 'useMxCheck' => filter_var( 72 | $this->node->getAttribute('data-validator-use-mx-check'), 73 | FILTER_VALIDATE_BOOLEAN 74 | ), 75 | ], 76 | ]; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/FormElement/File.php: -------------------------------------------------------------------------------- 1 | FileInput::class, 16 | 'name' => $this->getName(), 17 | 'required' => $this->isRequired(), 18 | ]; 19 | 20 | if ($this->node->hasAttribute('accept')) { 21 | $spec['validators'] = [ 22 | [ 23 | 'name' => MimeTypeValidator::class, 24 | 'options' => [ 25 | 'mimeType' => $this->node->getAttribute('accept'), 26 | 'enableHeaderCheck' => true, 27 | ], 28 | ], 29 | ]; 30 | } 31 | 32 | return $spec; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/FormElement/Hidden.php: -------------------------------------------------------------------------------- 1 | node->getAttribute('step') ?: 1; // Months 21 | $baseValue = $this->node->getAttribute('min') ?: '1970-01'; 22 | 23 | return [ 24 | 'name' => DateStepValidator::class, 25 | 'options' => [ 26 | 'format' => $this->format, 27 | 'baseValue' => $baseValue, 28 | 'step' => new DateInterval(sprintf('P%dM', $stepValue)), 29 | ], 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/FormElement/Number.php: -------------------------------------------------------------------------------- 1 | node->hasAttribute('min')) { 25 | $validators[] = $this->getMinValidator(); 26 | } 27 | 28 | if ($this->node->hasAttribute('max')) { 29 | $validators[] = $this->getMaxValidator(); 30 | } 31 | 32 | if ( 33 | ! $this->node->hasAttribute('step') 34 | || $this->node->getAttribute('step') !== 'any' 35 | ) { 36 | $validators[] = $this->getStepValidator(); 37 | } 38 | 39 | return $validators; 40 | } 41 | 42 | protected function getMinValidator(): array 43 | { 44 | return [ 45 | 'name' => GreaterThanValidator::class, 46 | 'options' => [ 47 | 'min' => $this->node->getAttribute('min'), 48 | 'inclusive' => true, 49 | ], 50 | ]; 51 | } 52 | 53 | protected function getMaxValidator(): array 54 | { 55 | return [ 56 | 'name' => LessThanValidator::class, 57 | 'options' => [ 58 | 'max' => $this->node->getAttribute('max'), 59 | 'inclusive' => true, 60 | ], 61 | ]; 62 | } 63 | 64 | protected function getStepValidator(): array 65 | { 66 | return [ 67 | 'name' => StepValidator::class, 68 | 'options' => [ 69 | 'baseValue' => $this->node->getAttribute('min') ?: 0, 70 | 'step' => $this->node->getAttribute('step') ?: 1, 71 | ], 72 | ]; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/FormElement/Password.php: -------------------------------------------------------------------------------- 1 | document); 19 | 20 | /** @var DOMElement $option */ 21 | foreach ($xpath->query('//input[@type="radio"][@name="' . $this->getName() . '"]') as $option) { 22 | $haystack[] = $option->getAttribute('value'); 23 | } 24 | 25 | $validators[] = [ 26 | 'name' => InArrayValidator::class, 27 | 'options' => ['haystack' => $haystack], 28 | ]; 29 | 30 | return $validators; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/FormElement/Range.php: -------------------------------------------------------------------------------- 1 | NumberValidator::class, 18 | ]; 19 | 20 | if ($this->node->hasAttribute('min')) { 21 | $validators[] = $this->getMinValidator(); 22 | } 23 | 24 | if ($this->node->hasAttribute('max')) { 25 | $validators[] = $this->getMaxValidator(); 26 | } 27 | 28 | if ( 29 | ! $this->node->hasAttribute('step') 30 | || $this->node->getAttribute('step') !== 'any' 31 | ) { 32 | $validators[] = $this->getStepValidator(); 33 | } 34 | 35 | return $validators; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/FormElement/Select.php: -------------------------------------------------------------------------------- 1 | node->hasAttribute('multiple')) { 18 | $validators[] = [ 19 | 'name' => ExplodeValidator::class, 20 | 'options' => [ 21 | 'validator' => $this->getInArrayValidator(), 22 | 'valueDelimiter' => null, // skip explode if only one value 23 | ], 24 | ]; 25 | } else { 26 | $validators[] = $this->getInArrayValidator(); 27 | } 28 | 29 | return $validators; 30 | } 31 | 32 | private function getInArrayValidator(): array 33 | { 34 | return [ 35 | 'name' => InArrayValidator::class, 36 | 'options' => [ 37 | 'haystack' => $this->getValueOptions(), 38 | 'strict' => false, 39 | ], 40 | ]; 41 | } 42 | 43 | private function getValueOptions(): array 44 | { 45 | $haystack = []; 46 | 47 | /** @var DOMElement $option */ 48 | foreach ($this->node->getElementsByTagName('option') as $option) { 49 | $haystack[] = $option->getAttribute('value'); 50 | } 51 | 52 | return $haystack; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/FormElement/Tel.php: -------------------------------------------------------------------------------- 1 | StripNewlinesFilter::class], 20 | ]; 21 | } 22 | 23 | protected function getValidators(): array 24 | { 25 | $validators = []; 26 | 27 | if ($this->node->hasAttribute('data-validator-country')) { 28 | // Only use the validator if a country is set 29 | $validators[] = [ 30 | 'name' => PhoneNumberValidator::class, 31 | 'options' => [ 32 | 'country' => $this->node->getAttribute('data-validator-country') ?: null, 33 | ], 34 | ]; 35 | } elseif (! $this->node->hasAttribute('pattern')) { 36 | // Use a very loose pattern for validation 37 | $this->node->setAttribute('pattern', '^\+[0-9]{1,3}[0-9\s]{4,17}$'); 38 | } 39 | 40 | if ($this->node->hasAttribute('minlength') || $this->node->hasAttribute('maxlength')) { 41 | $validators[] = [ 42 | 'name' => StringLengthValidator::class, 43 | 'options' => [ 44 | 'min' => $this->node->getAttribute('minlength') ?: 0, 45 | 'max' => $this->node->getAttribute('maxlength') ?: null, 46 | ], 47 | ]; 48 | } 49 | 50 | if ($this->node->hasAttribute('pattern')) { 51 | $validators[] = [ 52 | 'name' => RegexValidator::class, 53 | 'options' => [ 54 | 'pattern' => sprintf('/%s/', $this->node->getAttribute('pattern')), 55 | ], 56 | ]; 57 | } 58 | 59 | return $validators; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/FormElement/Text.php: -------------------------------------------------------------------------------- 1 | StripNewlinesFilter::class], 19 | ]; 20 | } 21 | 22 | protected function getValidators(): array 23 | { 24 | $validators = []; 25 | 26 | if ($this->node->hasAttribute('minlength') || $this->node->hasAttribute('maxlength')) { 27 | $validators[] = [ 28 | 'name' => StringLengthValidator::class, 29 | 'options' => [ 30 | 'min' => $this->node->getAttribute('minlength') ?: 0, 31 | 'max' => $this->node->getAttribute('maxlength') ?: null, 32 | ], 33 | ]; 34 | } 35 | 36 | if ($this->node->hasAttribute('pattern')) { 37 | $validators[] = [ 38 | 'name' => RegexValidator::class, 39 | 'options' => [ 40 | 'pattern' => sprintf('/%s/', $this->node->getAttribute('pattern')), 41 | ], 42 | ]; 43 | } 44 | 45 | return $validators; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/FormElement/Textarea.php: -------------------------------------------------------------------------------- 1 | node->hasAttribute('minlength') || $this->node->hasAttribute('maxlength')) { 16 | $validators[] = [ 17 | 'name' => StringLengthValidator::class, 18 | 'options' => [ 19 | 'min' => $this->node->getAttribute('minlength') ?: 0, 20 | 'max' => $this->node->getAttribute('maxlength') ?: null, 21 | ], 22 | ]; 23 | } 24 | 25 | return $validators; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/FormElement/Time.php: -------------------------------------------------------------------------------- 1 | node->getAttribute('step') ?: 60; // Seconds 22 | $baseValue = $this->node->getAttribute('min') ?: date($this->format, 0); 23 | 24 | return [ 25 | 'name' => DateStepValidator::class, 26 | 'options' => [ 27 | 'format' => $this->format, 28 | 'baseValue' => $baseValue, 29 | 'step' => new DateInterval(sprintf('PT%dS', $stepValue)), 30 | ], 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/FormElement/Url.php: -------------------------------------------------------------------------------- 1 | StripNewlinesFilter::class], 20 | ]; 21 | } 22 | 23 | protected function getValidators(): array 24 | { 25 | $validators = []; 26 | 27 | $validators[] = [ 28 | 'name' => UriValidator::class, 29 | 'options' => [ 30 | 'allowAbsolute' => true, 31 | 'allowRelative' => false, 32 | ], 33 | ]; 34 | 35 | if ($this->node->hasAttribute('minlength') || $this->node->hasAttribute('maxlength')) { 36 | $validators[] = [ 37 | 'name' => StringLengthValidator::class, 38 | 'options' => [ 39 | 'min' => $this->node->getAttribute('minlength') ?: 0, 40 | 'max' => $this->node->getAttribute('maxlength') ?: null, 41 | ], 42 | ]; 43 | } 44 | 45 | if ($this->node->hasAttribute('pattern')) { 46 | $validators[] = [ 47 | 'name' => RegexValidator::class, 48 | 'options' => [ 49 | 'pattern' => sprintf('/%s/', $this->node->getAttribute('pattern')), 50 | ], 51 | ]; 52 | } 53 | 54 | return $validators; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/FormElement/Week.php: -------------------------------------------------------------------------------- 1 | RegexValidator::class, 20 | 'options' => ['pattern' => '/^[0-9]{4}\-W[0-9]{2}$/'], 21 | ]; 22 | } 23 | 24 | protected function getStepValidator(): array 25 | { 26 | $stepValue = $this->node->getAttribute('step') ?: 1; // Weeks 27 | $baseValue = $this->node->getAttribute('min') ?: '1970-W01'; 28 | 29 | return [ 30 | 'name' => DateStepValidator::class, 31 | 'options' => [ 32 | 'format' => 'Y-\WW', 33 | 'baseValue' => $baseValue, 34 | 'step' => new DateInterval(sprintf('P%dW', $stepValue)), 35 | ], 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/FormFactory.php: -------------------------------------------------------------------------------- 1 | factory = $factory ?? new Factory(); 30 | $this->options = $options; 31 | } 32 | 33 | /** 34 | * @inheritDoc 35 | */ 36 | public function fromHtml( 37 | $html, 38 | ?array $defaultValues = null, 39 | ?InputFilterInterface $inputFilter = null 40 | ): FormInterface { 41 | // Create new doc 42 | $document = new DOMDocument('1.0', 'utf-8'); 43 | 44 | // Ignore invalid tag errors during loading (e.g. datalist) 45 | libxml_use_internal_errors(true); 46 | // Enforce UTF-8 encoding and don't add missing doctype, html and body 47 | $document->loadHTML( 48 | '' . $html, 49 | LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD 50 | ); 51 | libxml_use_internal_errors(false); 52 | 53 | return new Form( 54 | $document, 55 | $defaultValues ?? [], 56 | $this->factory, 57 | $inputFilter ?? $this->factory->createInputFilter([]), 58 | $this->options 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/FormFactoryFactory.php: -------------------------------------------------------------------------------- 1 | has(Factory::class)) { 16 | $factory = $container->get(Factory::class); 17 | } 18 | 19 | $options = $container->get('config')['html-form-validator'] ?? []; 20 | 21 | return new FormFactory($factory, $options); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/FormFactoryInterface.php: -------------------------------------------------------------------------------- 1 | get('config')['laminas-inputfilter'] ?? []; 18 | $filters = $config['filters'] ?? []; 19 | $validators = $config['validators'] ?? []; 20 | 21 | // Construct factory 22 | $factory = new Factory(new InputFilterPluginManager($container)); 23 | $factory 24 | ->getDefaultFilterChain() 25 | ->setPluginManager(new FilterPluginManager($container, $filters)); 26 | $factory 27 | ->getDefaultValidatorChain() 28 | ->setPluginManager(new ValidatorPluginManager($container, $validators)); 29 | 30 | return $factory; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ValidationResult.php: -------------------------------------------------------------------------------- 1 | rawValues = $rawValues; 35 | $this->values = $values; 36 | $this->messages = $messages; 37 | $this->method = $method; 38 | $this->submitName = $submitName; 39 | } 40 | 41 | /** 42 | * @inheritDoc 43 | */ 44 | public function isValid(): bool 45 | { 46 | return count($this->messages) === 0 && ($this->method === null || $this->method === 'POST'); 47 | } 48 | 49 | /** 50 | * @inheritDoc 51 | */ 52 | public function isClicked(string $name): bool 53 | { 54 | return $this->submitName === $name; 55 | } 56 | 57 | /** 58 | * @inheritDoc 59 | */ 60 | public function getClicked(): ?string 61 | { 62 | return $this->submitName; 63 | } 64 | 65 | /** 66 | * @inheritDoc 67 | */ 68 | public function addMessages(array $messages): void 69 | { 70 | $this->messages = array_replace_recursive($this->messages, $messages); 71 | } 72 | 73 | /** 74 | * @inheritDoc 75 | */ 76 | public function getMessages(): array 77 | { 78 | return $this->messages; 79 | } 80 | 81 | /** 82 | * @inheritDoc 83 | */ 84 | public function getRawValues(): array 85 | { 86 | return $this->rawValues; 87 | } 88 | 89 | /** 90 | * @inheritDoc 91 | */ 92 | public function getValues(): array 93 | { 94 | return $this->values; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/ValidationResultInterface.php: -------------------------------------------------------------------------------- 1 | ' => [ 34 | * '' => '', 35 | * ], 36 | * 'email' => [ 37 | * 'emailAddressInvalidFormat' => 'The given email address is invalid', 38 | * ], 39 | * ] 40 | */ 41 | public function addMessages(array $messages): void; 42 | 43 | /** 44 | * Get validation messages 45 | */ 46 | public function getMessages(): array; 47 | 48 | /** 49 | * Get the raw input values 50 | */ 51 | public function getRawValues(): array; 52 | 53 | /** 54 | * Get the filtered input values 55 | */ 56 | public function getValues(): array; 57 | } 58 | -------------------------------------------------------------------------------- /src/Validator/RecaptchaValidator.php: -------------------------------------------------------------------------------- 1 | 'ReCaptcha was invalid!']; 26 | 27 | /** @var array */ 28 | protected $options = ['key' => null]; 29 | 30 | /** 31 | * Sets validator options 32 | * 33 | * Accepts the following option keys: 34 | * 'key' => string, private recaptcha key 35 | * 36 | * @param array|Traversable $options 37 | * @throws InvalidArgumentException 38 | */ 39 | public function __construct($options = null) 40 | { 41 | if ($options instanceof Traversable) { 42 | $options = iterator_to_array($options); 43 | } 44 | 45 | if (! is_array($options) || ! array_key_exists('key', $options)) { 46 | throw new InvalidArgumentException('Missing private recaptcha key.'); 47 | } 48 | 49 | parent::__construct($options); 50 | } 51 | 52 | public function setKey(string $key): self 53 | { 54 | $this->options['key'] = $key; 55 | 56 | return $this; 57 | } 58 | 59 | /** @param mixed $value */ 60 | public function isValid($value): bool 61 | { 62 | $uri = sprintf(self::VERIFICATION_URI, $this->options['key'], $value); 63 | $json = file_get_contents($uri); 64 | $response = json_decode($json); 65 | 66 | if (! isset($response->success) || $response->success !== true) { 67 | $this->error(self::INVALID); 68 | 69 | return false; 70 | } 71 | 72 | return true; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/Fixtures/SAMPLE: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 10 |
11 | --DEFAULT-VALUES-- 12 | { 13 | "name": "default value" 14 | } 15 | --SUBMITTED-VALUES-- 16 | { 17 | "name": " Full Name " 18 | } 19 | --EXPECTED-VALUES-- 20 | { 21 | "name": "Full Name" 22 | } 23 | --EXPECTED-FORM-- 24 |
25 | 26 |
27 | --EXPECTED-ERRORS-- 28 | { 29 | "invalid": [ 30 | "notAlpha", 31 | "stringLengthTooShort" 32 | ] 33 | } 34 | --EXPECTED-EXCEPTION-- 35 | Exception 36 | -------------------------------------------------------------------------------- /test/Fixtures/attribute-aria-required.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test aria-required="true" attribute 3 | --HTML-FORM-- 4 |
5 | 6 | 7 | 8 | 9 | 10 |
11 | --DEFAULT-VALUES-- 12 | { 13 | } 14 | --SUBMITTED-VALUES-- 15 | { 16 | "valid_true": "Text", 17 | "valid_false": "Text", 18 | "valid_empty": "", 19 | 20 | "invalid_isempty": "" 21 | } 22 | --EXPECTED-VALUES-- 23 | { 24 | "valid_true": "Text", 25 | "valid_false": "Text", 26 | "valid_empty": "", 27 | 28 | "invalid_isempty": "" 29 | } 30 | --EXPECTED-FORM-- 31 | --EXPECTED-ERRORS-- 32 | { 33 | "invalid_isempty": { 34 | "isEmpty": "" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/Fixtures/attribute-data-filters.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test data filters attribute 3 | --HTML-FORM-- 4 |
5 | 6 | 7 | 8 |
9 | --DEFAULT-VALUES-- 10 | { 11 | } 12 | --SUBMITTED-VALUES-- 13 | { 14 | "valid_striptags": " Full Name ", 15 | "valid_stringtrim": " Full Name ", 16 | "valid_multiple_filters": " Full Name " 17 | } 18 | --EXPECTED-VALUES-- 19 | { 20 | "valid_striptags": " Full Name ", 21 | "valid_stringtrim": "Full Name ", 22 | "valid_multiple_filters": "Full Name" 23 | } 24 | --EXPECTED-FORM-- 25 | --EXPECTED-ERRORS-- 26 | { 27 | } 28 | -------------------------------------------------------------------------------- /test/Fixtures/attribute-data-input-name.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test data-input-name for custom elements 3 | --HTML-FORM-- 4 |
5 |
6 | 7 |
8 | --DEFAULT-VALUES-- 9 | { 10 | } 11 | --SUBMITTED-VALUES-- 12 | { 13 | "token": "1d79414c" 14 | } 15 | --EXPECTED-VALUES-- 16 | { 17 | "token": "1d79414c" 18 | } 19 | --EXPECTED-FORM-- 20 |
21 |
22 | 23 |
24 | --EXPECTED-ERRORS-- 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /test/Fixtures/attribute-data-validators-exception.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test data validators attribute 3 | --HTML-FORM-- 4 |
5 | 8 |
9 | --DEFAULT-VALUES-- 10 | { 11 | "name": "Bruce Wayne" 12 | } 13 | --SUBMITTED-VALUES-- 14 | { 15 | "name": " Batman " 16 | } 17 | --EXPECTED-VALUES-- 18 | { 19 | "name": " Batman " 20 | } 21 | --EXPECTED-FORM-- 22 | --EXPECTED-ERRORS-- 23 | { 24 | } 25 | --EXPECTED-EXCEPTION-- 26 | InvalidArgumentException 27 | -------------------------------------------------------------------------------- /test/Fixtures/attribute-data-validators.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test data validators attribute 3 | --HTML-FORM-- 4 |
5 | 6 | 9 | 12 | 13 | 16 | 19 | 22 |
23 | --DEFAULT-VALUES-- 24 | { 25 | } 26 | --SUBMITTED-VALUES-- 27 | { 28 | "valid_empty": "Bruce Wayne", 29 | "valid_stringlength": "Bruce Wayne", 30 | "valid_multiple": "Bruce Wayne", 31 | "invalid_multiple": "123", 32 | "invalid_toolong": "Bruce Wayne", 33 | "invalid_tooshort": "Bruce Wayne" 34 | } 35 | --EXPECTED-VALUES-- 36 | { 37 | "valid_empty": "Bruce Wayne", 38 | "valid_stringlength": "Bruce Wayne", 39 | "valid_multiple": "Bruce Wayne", 40 | "invalid_multiple": "123", 41 | "invalid_toolong": "Bruce Wayne", 42 | "invalid_tooshort": "Bruce Wayne" 43 | } 44 | --EXPECTED-FORM-- 45 | --EXPECTED-ERRORS-- 46 | { 47 | "invalid_multiple": { 48 | "stringLengthTooShort": "", 49 | "notAlpha": "" 50 | }, 51 | "invalid_toolong": { 52 | "stringLengthTooLong": "" 53 | }, 54 | "invalid_tooshort": { 55 | "stringLengthTooShort": "" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/Fixtures/attribute-disabled.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test aria-required="true" attribute 3 | --HTML-FORM-- 4 |
5 | 6 | 7 | 8 |
9 | --DEFAULT-VALUES-- 10 | { 11 | } 12 | --SUBMITTED-VALUES-- 13 | { 14 | } 15 | --EXPECTED-VALUES-- 16 | { 17 | } 18 | --EXPECTED-FORM-- 19 | --EXPECTED-ERRORS-- 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /test/Fixtures/attribute-maxlength.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | --DEFAULT-VALUES-- 22 | { 23 | } 24 | --SUBMITTED-VALUES-- 25 | { 26 | "valid_text": "example text", 27 | "valid_search": "example text", 28 | "valid_url": "http://example.com/", 29 | "valid_tel": "+34555666777", 30 | "valid_email": "email@example.com", 31 | "valid_password": "my secret password", 32 | "valid_textarea": "example text", 33 | 34 | "invalid_text": "example text", 35 | "invalid_search": "example text", 36 | "invalid_url": "http://example.com/", 37 | "invalid_tel": "+34555666777", 38 | "invalid_email": "email@example.com", 39 | "invalid_password": "my secret password", 40 | "invalid_textarea": "example text" 41 | } 42 | --EXPECTED-VALUES-- 43 | { 44 | "valid_text": "example text", 45 | "valid_search": "example text", 46 | "valid_url": "http://example.com/", 47 | "valid_tel": "+34555666777", 48 | "valid_email": "email@example.com", 49 | "valid_password": "my secret password", 50 | "valid_textarea": "example text", 51 | 52 | "invalid_text": "example text", 53 | "invalid_search": "example text", 54 | "invalid_url": "http://example.com/", 55 | "invalid_tel": "+34555666777", 56 | "invalid_email": "email@example.com", 57 | "invalid_password": "my secret password", 58 | "invalid_textarea": "example text" 59 | } 60 | --EXPECTED-FORM-- 61 | --EXPECTED-ERRORS-- 62 | { 63 | "invalid_text": { 64 | "stringLengthTooLong": "" 65 | }, 66 | "invalid_search": { 67 | "stringLengthTooLong": "" 68 | }, 69 | "invalid_url": { 70 | "stringLengthTooLong": "" 71 | }, 72 | "invalid_tel": { 73 | "stringLengthTooLong": "" 74 | }, 75 | "invalid_email": { 76 | "stringLengthTooLong": "" 77 | }, 78 | "invalid_password": { 79 | "stringLengthTooLong": "" 80 | }, 81 | "invalid_textarea": { 82 | "stringLengthTooLong": "" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test/Fixtures/attribute-minlength.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | --DEFAULT-VALUES-- 22 | { 23 | } 24 | --SUBMITTED-VALUES-- 25 | { 26 | "valid_text": "example text", 27 | "valid_search": "example text", 28 | "valid_url": "http://example.com/", 29 | "valid_tel": "+34555666777", 30 | "valid_email": "email@example.com", 31 | "valid_password": "my secret password", 32 | "valid_textarea": "example text", 33 | 34 | "invalid_text": "example text", 35 | "invalid_search": "example text", 36 | "invalid_url": "http://example.com/", 37 | "invalid_tel": "+34555666777", 38 | "invalid_email": "email@example.com", 39 | "invalid_password": "my secret password", 40 | "invalid_textarea": "example text" 41 | } 42 | --EXPECTED-VALUES-- 43 | { 44 | "valid_text": "example text", 45 | "valid_search": "example text", 46 | "valid_url": "http://example.com/", 47 | "valid_tel": "+34555666777", 48 | "valid_email": "email@example.com", 49 | "valid_password": "my secret password", 50 | "valid_textarea": "example text", 51 | 52 | "invalid_text": "example text", 53 | "invalid_search": "example text", 54 | "invalid_url": "http://example.com/", 55 | "invalid_tel": "+34555666777", 56 | "invalid_email": "email@example.com", 57 | "invalid_password": "my secret password", 58 | "invalid_textarea": "example text" 59 | } 60 | --EXPECTED-FORM-- 61 | --EXPECTED-ERRORS-- 62 | { 63 | "invalid_text": { 64 | "stringLengthTooShort": "" 65 | }, 66 | "invalid_search": { 67 | "stringLengthTooShort": "" 68 | }, 69 | "invalid_url": { 70 | "stringLengthTooShort": "" 71 | }, 72 | "invalid_tel": { 73 | "stringLengthTooShort": "" 74 | }, 75 | "invalid_email": { 76 | "stringLengthTooShort": "" 77 | }, 78 | "invalid_password": { 79 | "stringLengthTooShort": "" 80 | }, 81 | "invalid_textarea": { 82 | "stringLengthTooShort": "" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test/Fixtures/attribute-pattern.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | --DEFAULT-VALUES-- 20 | { 21 | } 22 | --SUBMITTED-VALUES-- 23 | { 24 | "valid_text": "example text", 25 | "valid_search": "example text", 26 | "valid_url": "http://example.com/", 27 | "valid_tel": "+34555666777", 28 | "valid_email": "email@example.com", 29 | "valid_password": "my secret password", 30 | 31 | "invalid_text": "example text", 32 | "invalid_search": "example text", 33 | "invalid_url": "http://example.com/", 34 | "invalid_tel": "+34555666777", 35 | "invalid_email": "email@example.com", 36 | "invalid_password": "my secret password" 37 | } 38 | --EXPECTED-VALUES-- 39 | { 40 | "valid_text": "example text", 41 | "valid_search": "example text", 42 | "valid_url": "http://example.com/", 43 | "valid_tel": "+34555666777", 44 | "valid_email": "email@example.com", 45 | "valid_password": "my secret password", 46 | 47 | "invalid_text": "example text", 48 | "invalid_search": "example text", 49 | "invalid_url": "http://example.com/", 50 | "invalid_tel": "+34555666777", 51 | "invalid_email": "email@example.com", 52 | "invalid_password": "my secret password" 53 | } 54 | --EXPECTED-FORM-- 55 | --EXPECTED-ERRORS-- 56 | { 57 | "invalid_text": { 58 | "regexNotMatch": "" 59 | }, 60 | "invalid_search": { 61 | "regexNotMatch": "" 62 | }, 63 | "invalid_url": { 64 | "regexNotMatch": "" 65 | }, 66 | "invalid_tel": { 67 | "regexNotMatch": "" 68 | }, 69 | "invalid_email": { 70 | "regexNotMatch": "" 71 | }, 72 | "invalid_password": { 73 | "regexNotMatch": "" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/Fixtures/attribute-required.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test required attribute 3 | --HTML-FORM-- 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | --DEFAULT-VALUES-- 13 | { 14 | } 15 | --SUBMITTED-VALUES-- 16 | { 17 | "valid_short": "This is a test", 18 | "valid_long": "This is a test", 19 | "valid_not_required": "", 20 | "invalid_short": "", 21 | "invalid_long": "" 22 | } 23 | --EXPECTED-VALUES-- 24 | { 25 | "valid_short": "This is a test", 26 | "valid_long": "This is a test", 27 | "valid_not_required": "", 28 | "invalid_short": "", 29 | "invalid_long": "" 30 | } 31 | --EXPECTED-FORM-- 32 | --EXPECTED-ERRORS-- 33 | { 34 | "invalid_short": { 35 | "isEmpty": "" 36 | }, 37 | "invalid_long": { 38 | "isEmpty": "" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/Fixtures/form-contact-example.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Example contact form 3 | --HTML-FORM-- 4 |
5 |
6 |
7 |
8 | 9 | 12 |
13 |
14 |
15 |
16 | 17 | 20 |
21 |
22 |
23 | 24 |
25 | 26 | 29 |
30 | 31 |
32 | 33 | 36 |
37 | 38 | 39 | 40 | 41 | 42 |
43 | --DEFAULT-VALUES-- 44 | { 45 | } 46 | --SUBMITTED-VALUES-- 47 | { 48 | "name": " John Doe ", 49 | "email": "john.doe@example.com", 50 | "subject": "Subject", 51 | "body": "Message body", 52 | "token": "1d79414c" 53 | } 54 | --EXPECTED-VALUES-- 55 | { 56 | "name": "John Doe", 57 | "email": "john.doe@example.com", 58 | "subject": "Subject", 59 | "body": "Message body", 60 | "token": "1d79414c" 61 | } 62 | --EXPECTED-FORM-- 63 | --EXPECTED-ERRORS-- 64 | { 65 | } 66 | -------------------------------------------------------------------------------- /test/Fixtures/form-render-error.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 |
6 | 7 |
8 |
9 | --DEFAULT-VALUES-- 10 | { 11 | } 12 | --SUBMITTED-VALUES-- 13 | { 14 | "name": "" 15 | } 16 | --EXPECTED-VALUES-- 17 | { 18 | "name": "" 19 | } 20 | --EXPECTED-FORM-- 21 |
22 |
23 | 24 |
Value is required and can't be empty
25 |
26 |
27 | --EXPECTED-ERRORS-- 28 | { 29 | "name": { 30 | "isEmpty": "" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/Fixtures/form-render-errors.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Test data validators attribute 3 | --HTML-FORM-- 4 |
5 |
6 | 9 |
10 |
11 | --DEFAULT-VALUES-- 12 | { 13 | "name": "Bruce Wayne" 14 | } 15 | --SUBMITTED-VALUES-- 16 | { 17 | "name": " Batman " 18 | } 19 | --EXPECTED-VALUES-- 20 | { 21 | "name": " Batman " 22 | } 23 | --EXPECTED-FORM-- 24 |
25 |
26 | 27 |
The input contains non alphabetic characters
28 |
The input is less than 32 characters long
29 |
30 |
31 | --EXPECTED-ERRORS-- 32 | { 33 | "name": { 34 | "notAlpha": "", 35 | "stringLengthTooShort": "" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/Fixtures/select-render.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 16 | 17 | 22 | 23 | 28 | 29 | 34 | 35 | 40 |
41 | --DEFAULT-VALUES-- 42 | { 43 | "valid_optgroup": "audi", 44 | "valid_default_empty": "audi", 45 | "valid_default_value": "audi" 46 | } 47 | --SUBMITTED-VALUES-- 48 | { 49 | "valid_optgroup": "bugatti", 50 | "valid_no_default_empty": "", 51 | "valid_default_empty": "", 52 | "valid_default_value": "bugatti", 53 | "valid_no_default_value": "volkswagen" 54 | } 55 | --EXPECTED-VALUES-- 56 | { 57 | "valid_optgroup": "bugatti", 58 | "valid_no_default_empty": "", 59 | "valid_default_empty": "", 60 | "valid_default_value": "bugatti", 61 | "valid_no_default_value": "volkswagen" 62 | } 63 | --EXPECTED-FORM-- 64 |
65 | 76 | 77 | 82 | 83 | 88 | 89 | 94 | 95 | 100 |
101 | --EXPECTED-ERRORS-- 102 | { 103 | } 104 | -------------------------------------------------------------------------------- /test/Fixtures/select.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 10 | 11 | 16 | 17 | 22 | 23 | 28 | 29 | 34 | 35 | 41 |
42 | --DEFAULT-VALUES-- 43 | { 44 | } 45 | --SUBMITTED-VALUES-- 46 | { 47 | "valid": "audi", 48 | "valid_multiple": ["audi","bugatti"], 49 | "valid_required": "audi", 50 | 51 | "invalid": "mercedes", 52 | "invalid_multiple": ["audi","mercedes"], 53 | "invalid_required": "mercedes" 54 | } 55 | --EXPECTED-VALUES-- 56 | { 57 | "valid": "audi", 58 | "valid_multiple": ["audi","bugatti"], 59 | "valid_required": "audi", 60 | 61 | "invalid": "mercedes", 62 | "invalid_multiple": ["audi","mercedes"], 63 | "invalid_required": "mercedes" 64 | } 65 | --EXPECTED-FORM-- 66 | --EXPECTED-ERRORS-- 67 | { 68 | "invalid": { 69 | "notInArray": "" 70 | }, 71 | "invalid_multiple": [{ 72 | "notInArray": "" 73 | }], 74 | "invalid_required": { 75 | "notInArray": "" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/Fixtures/textarea.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 6 | 7 |
8 | --DEFAULT-VALUES-- 9 | { 10 | "valid_default": "Minions ipsum para tu hahaha wiiiii ti aamoo! Tank yuuu!", 11 | "valid_reuse": "Minions ipsum para tu hahaha wiiiii ti aamoo! Tank yuuu!" 12 | } 13 | --SUBMITTED-VALUES-- 14 | { 15 | "valid_default": "Tatata bala tu wiiiii pepete la bodaaa me want bananaaa!", 16 | "valid_reuse": "Tatata bala tu wiiiii pepete la bodaaa me want bananaaa!" 17 | } 18 | --EXPECTED-VALUES-- 19 | { 20 | "valid_default": "Tatata bala tu wiiiii pepete la bodaaa me want bananaaa!", 21 | "valid_reuse": "Tatata bala tu wiiiii pepete la bodaaa me want bananaaa!" 22 | } 23 | --EXPECTED-FORM-- 24 |
25 | 26 | 27 |
28 | --EXPECTED-ERRORS-- 29 | { 30 | } 31 | -------------------------------------------------------------------------------- /test/Fixtures/type-checkbox.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 6 | 7 | 8 |
9 | --DEFAULT-VALUES-- 10 | { 11 | } 12 | --SUBMITTED-VALUES-- 13 | { 14 | "valid_empty": "", 15 | "valid_selected": "value", 16 | "invalid": "invalid" 17 | } 18 | --EXPECTED-VALUES-- 19 | { 20 | "valid_empty": "", 21 | "valid_selected": "value", 22 | "invalid": "invalid" 23 | } 24 | --EXPECTED-FORM-- 25 |
26 | 27 | 28 | 29 |
The two given tokens do not match
30 |
31 | --EXPECTED-ERRORS-- 32 | { 33 | "invalid": { 34 | "notSame": "" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/Fixtures/type-color.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 6 | 7 | 8 |
9 | --DEFAULT-VALUES-- 10 | { 11 | } 12 | --SUBMITTED-VALUES-- 13 | { 14 | "valid": "#fefefe", 15 | "invalid": "01234", 16 | "filter_lowercase": "#FEFEFE" 17 | } 18 | --EXPECTED-VALUES-- 19 | { 20 | "valid": "#fefefe", 21 | "invalid": "01234", 22 | "filter_lowercase": "#fefefe" 23 | } 24 | --EXPECTED-FORM-- 25 | --EXPECTED-ERRORS-- 26 | { 27 | "invalid": { 28 | "regexNotMatch": "" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/Fixtures/type-date.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | --DEFAULT-VALUES-- 18 | { 19 | } 20 | --SUBMITTED-VALUES-- 21 | { 22 | "valid": "1997-08-29", 23 | "valid_min": "1997-08-29", 24 | "valid_max": "1997-08-29", 25 | "valid_step": "1997-08-29", 26 | "valid_min_step": "1997-08-29", 27 | "invalid_date": "2016-02-31", 28 | "invalid_format": "29-08-1997", 29 | "invalid_min": "1997-08-29", 30 | "invalid_max": "1997-08-29", 31 | "invalid_step": "1997-08-29", 32 | "invalid_min_step": "1997-08-29" 33 | } 34 | --EXPECTED-VALUES-- 35 | { 36 | "valid": "1997-08-29", 37 | "valid_min": "1997-08-29", 38 | "valid_max": "1997-08-29", 39 | "valid_step": "1997-08-29", 40 | "valid_min_step": "1997-08-29", 41 | "invalid_date": "2016-02-31", 42 | "invalid_format": "29-08-1997", 43 | "invalid_min": "1997-08-29", 44 | "invalid_max": "1997-08-29", 45 | "invalid_step": "1997-08-29", 46 | "invalid_min_step": "1997-08-29" 47 | } 48 | --EXPECTED-FORM-- 49 | --EXPECTED-ERRORS-- 50 | { 51 | "invalid_date": { 52 | "dateFalseFormat": "", 53 | "dateInvalidDate": "" 54 | }, 55 | "invalid_format": { 56 | "dateInvalidDate": "" 57 | }, 58 | "invalid_min": { 59 | "notGreaterThanInclusive": "" 60 | }, 61 | "invalid_max": { 62 | "notLessThanInclusive": "" 63 | }, 64 | "invalid_step": { 65 | "dateStepNotStep": "" 66 | }, 67 | "invalid_min_step": { 68 | "dateStepNotStep": "" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/Fixtures/type-datetime-local.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | --DEFAULT-VALUES-- 18 | { 19 | } 20 | --SUBMITTED-VALUES-- 21 | { 22 | "valid": "1997-08-29T02:14", 23 | "valid_min": "1997-08-29T02:14", 24 | "valid_max": "1997-08-29T02:14", 25 | "valid_step": "1997-08-29T02:14", 26 | "valid_min_step": "1997-08-29T02:14", 27 | "invalid_date": "1997-02-31T02:14", 28 | "invalid_format": "29-08-1997T02:14", 29 | "invalid_min": "1997-08-29T02:14", 30 | "invalid_max": "1997-08-29T02:14", 31 | "invalid_step": "1997-08-29T02:14", 32 | "invalid_min_step": "1997-08-29T02:14" 33 | } 34 | --EXPECTED-VALUES-- 35 | { 36 | "valid": "1997-08-29T02:14", 37 | "valid_min": "1997-08-29T02:14", 38 | "valid_max": "1997-08-29T02:14", 39 | "valid_step": "1997-08-29T02:14", 40 | "valid_min_step": "1997-08-29T02:14", 41 | "invalid_date": "1997-02-31T02:14", 42 | "invalid_format": "29-08-1997T02:14", 43 | "invalid_min": "1997-08-29T02:14", 44 | "invalid_max": "1997-08-29T02:14", 45 | "invalid_step": "1997-08-29T02:14", 46 | "invalid_min_step": "1997-08-29T02:14" 47 | } 48 | --EXPECTED-FORM-- 49 | --EXPECTED-ERRORS-- 50 | { 51 | "invalid_date": { 52 | "dateFalseFormat": "", 53 | "dateInvalidDate": "" 54 | }, 55 | "invalid_format": { 56 | "dateFalseFormat": "", 57 | "dateInvalidDate": "" 58 | }, 59 | "invalid_min": { 60 | "notGreaterThanInclusive": "" 61 | }, 62 | "invalid_max": { 63 | "notLessThanInclusive": "" 64 | }, 65 | "invalid_step": { 66 | "dateStepNotStep": "" 67 | }, 68 | "invalid_min_step": { 69 | "dateStepNotStep": "" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /test/Fixtures/type-email.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | --DEFAULT-VALUES-- 14 | --SUBMITTED-VALUES-- 15 | { 16 | "valid": "john.doe@example.com", 17 | "valid_multiple": "john.doe@example.com,jane.doe@example.com", 18 | "invalid": "john.doe@example", 19 | "invalid_multiple": "john.doe@example.com,jane.doe@example", 20 | "invalid_multiple_format": "john.doe@example.com|jane.doe@example.com", 21 | "filter_stripnewlines": "john.doe@example.com\n", 22 | "filter_stringtrim": " john.doe@example.com " 23 | } 24 | --EXPECTED-VALUES-- 25 | { 26 | "valid": "john.doe@example.com", 27 | "valid_multiple": "john.doe@example.com,jane.doe@example.com", 28 | "invalid": "john.doe@example", 29 | "invalid_multiple": "john.doe@example.com,jane.doe@example", 30 | "invalid_multiple_format": "john.doe@example.com|jane.doe@example.com", 31 | "filter_stripnewlines": "john.doe@example.com", 32 | "filter_stringtrim": "john.doe@example.com" 33 | } 34 | --EXPECTED-FORM-- 35 | --EXPECTED-ERRORS-- 36 | { 37 | "invalid": { 38 | "emailAddressInvalidHostname": "", 39 | "hostnameInvalidHostname": "", 40 | "hostnameLocalNameNotAllowed": "" 41 | }, 42 | "invalid_multiple": [{ 43 | "emailAddressInvalidHostname": "", 44 | "hostnameInvalidHostname": "", 45 | "hostnameLocalNameNotAllowed": "" 46 | }], 47 | "invalid_multiple_format": [{ 48 | "emailAddressDotAtom": "", 49 | "emailAddressQuotedString": "", 50 | "emailAddressInvalidLocalPart": "" 51 | }] 52 | } 53 | -------------------------------------------------------------------------------- /test/Fixtures/type-file.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 6 |
7 | --DEFAULT-VALUES-- 8 | { 9 | } 10 | --SUBMITTED-VALUES-- 11 | { 12 | "file": "" 13 | } 14 | --EXPECTED-VALUES-- 15 | { 16 | "file": "" 17 | } 18 | --EXPECTED-FORM-- 19 | --EXPECTED-ERRORS-- 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /test/Fixtures/type-month.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | --DEFAULT-VALUES-- 18 | { 19 | } 20 | --SUBMITTED-VALUES-- 21 | { 22 | "valid": "1997-08", 23 | "valid_min": "1997-08", 24 | "valid_max": "1997-08", 25 | "valid_step": "1997-08", 26 | "valid_min_step": "1997-08", 27 | "invalid_date": "2016 08 02", 28 | "invalid_format": "20-08-1997", 29 | "invalid_min": "1997-08", 30 | "invalid_max": "1997-08", 31 | "invalid_step": "1997-08", 32 | "invalid_min_step": "1997-08" 33 | } 34 | --EXPECTED-VALUES-- 35 | { 36 | "valid": "1997-08", 37 | "valid_min": "1997-08", 38 | "valid_max": "1997-08", 39 | "valid_step": "1997-08", 40 | "valid_min_step": "1997-08", 41 | "invalid_date": "2016 08 02", 42 | "invalid_format": "20-08-1997", 43 | "invalid_min": "1997-08", 44 | "invalid_max": "1997-08", 45 | "invalid_step": "1997-08", 46 | "invalid_min_step": "1997-08" 47 | } 48 | --EXPECTED-FORM-- 49 | --EXPECTED-ERRORS-- 50 | { 51 | "invalid_date": { 52 | "dateInvalidDate": "" 53 | }, 54 | "invalid_format": { 55 | "dateInvalidDate": "" 56 | }, 57 | "invalid_min": { 58 | "notGreaterThanInclusive": "" 59 | }, 60 | "invalid_max": { 61 | "notLessThanInclusive": "" 62 | }, 63 | "invalid_step": { 64 | "dateStepNotStep": "" 65 | }, 66 | "invalid_min_step": { 67 | "dateStepNotStep": "" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/Fixtures/type-number.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Tests the number input type. The range input type works exactly the same. 3 | --HTML-FORM-- 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | --DEFAULT-VALUES-- 25 | { 26 | } 27 | --SUBMITTED-VALUES-- 28 | { 29 | "valid_int": "6", 30 | "valid_min": "6", 31 | "valid_min_edge": "4", 32 | "valid_max": "6", 33 | "valid_max_edge": "4", 34 | "valid_step_int": "6", 35 | "valid_step_float": "6.01", 36 | "valid_step_any_int": "1", 37 | "valid_step_any_float": "6.01", 38 | "valid_between_default": "6", 39 | "valid_between_step": "6", 40 | 41 | "invalid_text": "abcd", 42 | "invalid_min": "2", 43 | "invalid_max": "10", 44 | "invalid_step": "1", 45 | "invalid_between_step": "3", 46 | "invalid_step_format": "6" 47 | } 48 | --EXPECTED-VALUES-- 49 | { 50 | "valid_int": "6", 51 | "valid_min": "6", 52 | "valid_min_edge": "4", 53 | "valid_max": "6", 54 | "valid_max_edge": "4", 55 | "valid_step_int": "6", 56 | "valid_step_float": "6.01", 57 | "valid_step_any_int": "1", 58 | "valid_step_any_float": "6.01", 59 | "valid_between_default": "6", 60 | "valid_between_step": "6", 61 | 62 | "invalid_text": "abcd", 63 | "invalid_min": "2", 64 | "invalid_max": "10", 65 | "invalid_step": "1", 66 | "invalid_between_step": "3", 67 | "invalid_step_format": "6" 68 | } 69 | --EXPECTED-FORM-- 70 | --EXPECTED-ERRORS-- 71 | { 72 | "invalid_text": { 73 | "regexNotMatch": "", 74 | "typeInvalid": "" 75 | }, 76 | "invalid_min": { 77 | "notGreaterThanInclusive": "" 78 | }, 79 | "invalid_max": { 80 | "notLessThanInclusive": "" 81 | }, 82 | "invalid_step": { 83 | "stepInvalid": "" 84 | }, 85 | "invalid_between_step": { 86 | "notGreaterThanInclusive": "", 87 | "stepInvalid": "" 88 | }, 89 | "invalid_step_format" : { 90 | "stepInvalid": "" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /test/Fixtures/type-password.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 6 | 7 | 8 | 9 | 10 |
11 | --DEFAULT-VALUES-- 12 | { 13 | } 14 | --SUBMITTED-VALUES-- 15 | { 16 | "valid": "123456", 17 | "valid_confirm": "123456", 18 | "invalid": "123456", 19 | "invalid_confirm": "12345", 20 | "filter_stripnewlines": "my\npassword" 21 | } 22 | --EXPECTED-VALUES-- 23 | { 24 | "valid": "123456", 25 | "valid_confirm": "123456", 26 | "invalid": "123456", 27 | "invalid_confirm": "12345", 28 | "filter_stripnewlines": "mypassword" 29 | } 30 | --EXPECTED-FORM-- 31 | --EXPECTED-ERRORS-- 32 | { 33 | "invalid_confirm": { 34 | "notSame": "" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/Fixtures/type-radio.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | --DEFAULT-VALUES-- 21 | { 22 | "valid_default": "male" 23 | } 24 | --SUBMITTED-VALUES-- 25 | { 26 | "valid": "male", 27 | "valid_default": "male", 28 | "valid_reuse": "female", 29 | "invalid": "other", 30 | "invalid_reuse": "other" 31 | } 32 | --EXPECTED-VALUES-- 33 | { 34 | "valid": "male", 35 | "valid_default": "male", 36 | "valid_reuse": "female", 37 | "invalid": "other", 38 | "invalid_reuse": "other" 39 | } 40 | --EXPECTED-FORM-- 41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
The input was not found in the haystack
53 | 54 | 55 | 56 |
The input was not found in the haystack
57 | 58 |
59 | --EXPECTED-ERRORS-- 60 | { 61 | "invalid": { 62 | "notInArray": "" 63 | }, 64 | "invalid_reuse": { 65 | "notInArray": "" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/Fixtures/type-search.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 6 |
7 | --DEFAULT-VALUES-- 8 | { 9 | } 10 | --SUBMITTED-VALUES-- 11 | { 12 | "query": "Batman" 13 | } 14 | --EXPECTED-VALUES-- 15 | { 16 | "query": "Batman" 17 | } 18 | --EXPECTED-FORM-- 19 | --EXPECTED-ERRORS-- 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /test/Fixtures/type-tel.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | --DEFAULT-VALUES-- 13 | { 14 | } 15 | --SUBMITTED-VALUES-- 16 | { 17 | "valid": "+34555666777", 18 | "valid_default_pattern": "+31 123 456 789", 19 | "invalid_default_pattern": "+31 (0) 123-456-789", 20 | "invalid_tel": "556777", 21 | "invalid_country": "+31555666777", 22 | "filter_stripnewlines": "+34\n555666777" 23 | } 24 | --EXPECTED-VALUES-- 25 | { 26 | "valid": "+34555666777", 27 | "valid_default_pattern": "+31 123 456 789", 28 | "invalid_default_pattern": "+31 (0) 123-456-789", 29 | "invalid_tel": "556777", 30 | "invalid_country": "+31555666777", 31 | "filter_stripnewlines": "+34555666777" 32 | } 33 | --EXPECTED-FORM-- 34 |
35 | 36 | 37 | 38 |
The input does not match against pattern '/^\+[0-9]{1,3}[0-9\s]{4,17}$/'
39 | 40 |
The input does not match a phone number format
41 | 42 |
The input does not match a phone number format
43 | 44 |
45 | --EXPECTED-ERRORS-- 46 | { 47 | "invalid_default_pattern": { 48 | "regexNotMatch": "" 49 | }, 50 | "invalid_tel": { 51 | "phoneNumberNoMatch": "" 52 | }, 53 | "invalid_country": { 54 | "phoneNumberNoMatch": "" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/Fixtures/type-text.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 6 | 7 |
8 | --DEFAULT-VALUES-- 9 | { 10 | } 11 | --SUBMITTED-VALUES-- 12 | { 13 | "name": "Batman", 14 | "filter_stripnewlines": "Bat\nman" 15 | } 16 | --EXPECTED-VALUES-- 17 | { 18 | "name": "Batman", 19 | "filter_stripnewlines": "Batman" 20 | } 21 | --EXPECTED-FORM-- 22 | --EXPECTED-ERRORS-- 23 | { 24 | } 25 | -------------------------------------------------------------------------------- /test/Fixtures/type-time.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | --DEFAULT-VALUES-- 18 | { 19 | } 20 | --SUBMITTED-VALUES-- 21 | { 22 | "valid": "02:14:00", 23 | "valid_min": "02:14:00", 24 | "valid_max": "02:14:00", 25 | "valid_step": "02:14:00", 26 | "valid_min_step": "02:14:00", 27 | "invalid_value": "25:00:00", 28 | "invalid_format": "2016-02-08 22:00", 29 | "invalid_min": "02:14:00", 30 | "invalid_max": "02:14:00", 31 | "invalid_step": "02:14:00", 32 | "invalid_min_step": "02:14:00" 33 | } 34 | --EXPECTED-VALUES-- 35 | { 36 | "valid": "02:14:00", 37 | "valid_min": "02:14:00", 38 | "valid_max": "02:14:00", 39 | "valid_step": "02:14:00", 40 | "valid_min_step": "02:14:00", 41 | "invalid_value": "25:00:00", 42 | "invalid_format": "2016-02-08 22:00", 43 | "invalid_min": "02:14:00", 44 | "invalid_max": "02:14:00", 45 | "invalid_step": "02:14:00", 46 | "invalid_min_step": "02:14:00" 47 | } 48 | --EXPECTED-FORM-- 49 | --EXPECTED-ERRORS-- 50 | { 51 | "invalid_value": { 52 | "dateFalseFormat": "", 53 | "dateInvalidDate": "" 54 | }, 55 | "invalid_format": { 56 | "dateInvalidDate": "" 57 | }, 58 | "invalid_min": { 59 | "notGreaterThanInclusive": "" 60 | }, 61 | "invalid_max": { 62 | "notLessThanInclusive": "" 63 | }, 64 | "invalid_step": { 65 | "dateStepNotStep": "" 66 | }, 67 | "invalid_min_step": { 68 | "dateStepNotStep": "" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/Fixtures/type-url.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 6 | 7 | 8 | 9 |
10 | --DEFAULT-VALUES-- 11 | { 12 | } 13 | --SUBMITTED-VALUES-- 14 | { 15 | "valid": "https://example.com/foo/bar", 16 | "invalid_relative": "/foo/bar", 17 | "invalid_missing_protocol": "//example.com/foo/bar", 18 | "filter_stripnewlines": "https://example.com\n/\nfoo\n/bar" 19 | } 20 | --EXPECTED-VALUES-- 21 | { 22 | "valid": "https://example.com/foo/bar", 23 | "invalid_relative": "/foo/bar", 24 | "invalid_missing_protocol": "//example.com/foo/bar", 25 | "filter_stripnewlines": "https://example.com/foo/bar" 26 | } 27 | --EXPECTED-FORM-- 28 | --EXPECTED-ERRORS-- 29 | { 30 | "invalid_relative": { 31 | "notUri": "" 32 | }, 33 | "invalid_missing_protocol": { 34 | "notUri": "" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/Fixtures/type-week.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | --DEFAULT-VALUES-- 18 | { 19 | } 20 | --SUBMITTED-VALUES-- 21 | { 22 | "valid": "1997-W35", 23 | "valid_min": "1997-W35", 24 | "valid_max": "1997-W35", 25 | "valid_step": "1997-W35", 26 | "valid_min_step": "1997-W35", 27 | "invalid": "1997-35", 28 | "invalid_min": "1997-W35", 29 | "invalid_max": "1997-W35", 30 | "invalid_step": "1997-W35", 31 | "invalid_min_step": "1997-W35" 32 | } 33 | --EXPECTED-VALUES-- 34 | { 35 | "valid": "1997-W35", 36 | "valid_min": "1997-W35", 37 | "valid_max": "1997-W35", 38 | "valid_step": "1997-W35", 39 | "valid_min_step": "1997-W35", 40 | "invalid": "1997-35", 41 | "invalid_min": "1997-W35", 42 | "invalid_max": "1997-W35", 43 | "invalid_step": "1997-W35", 44 | "invalid_min_step": "1997-W35" 45 | } 46 | --EXPECTED-FORM-- 47 | --EXPECTED-ERRORS-- 48 | { 49 | "invalid": { 50 | "regexNotMatch": "", 51 | "dateInvalidDate": "" 52 | }, 53 | "invalid_min": { 54 | "notGreaterThanInclusive": "" 55 | }, 56 | "invalid_max": { 57 | "notLessThanInclusive": "" 58 | }, 59 | "invalid_step": { 60 | "dateStepNotStep": "" 61 | }, 62 | "invalid_min_step": { 63 | "dateStepNotStep": "" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/Fixtures/utf8-encoding.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | 3 | --HTML-FORM-- 4 |
5 | 6 | 7 | 8 |
9 | --DEFAULT-VALUES-- 10 | { 11 | "valid_default": "Îñţérñåţîöñåļîžåţîöñ" 12 | } 13 | --SUBMITTED-VALUES-- 14 | { 15 | "valid_value": "Îñţérñåţîöñåļîžåţîöñ", 16 | "valid_default": "Îñţérñåţîöñåļîžåţîöñ", 17 | "valid_reuse": "Îñţérñåţîöñåļîžåţîöñ" 18 | } 19 | --EXPECTED-VALUES-- 20 | { 21 | "valid_value": "Îñţérñåţîöñåļîžåţîöñ", 22 | "valid_default": "Îñţérñåţîöñåļîžåţîöñ", 23 | "valid_reuse": "Îñţérñåţîöñåļîžåţîöñ" 24 | } 25 | --EXPECTED-FORM-- 26 |
27 | 28 | 29 | 30 |
31 | -------------------------------------------------------------------------------- /test/FormElement/BaseFormElementTest.php: -------------------------------------------------------------------------------- 1 | [ 24 | 'stringtrim', 25 | [ 26 | 'stringtrim' => [], 27 | ], 28 | ], 29 | 'multiple-filters' => [ 30 | 'stringtrim|alpha', 31 | [ 32 | 'stringtrim' => [], 33 | 'alpha' => [], 34 | ], 35 | ], 36 | 'single-option' => [ 37 | 'identical{token:password}', 38 | [ 39 | 'identical' => ['token' => 'password'], 40 | ], 41 | ], 42 | 'multiple-options' => [ 43 | 'stringlength{min:2,max:140}', 44 | [ 45 | 'stringlength' => [ 46 | 'min' => '2', 47 | 'max' => '140', 48 | ], 49 | ], 50 | ], 51 | 'options-with-spaces' => [ 52 | 'validator{key: va l ue , foo : bar , baz:qux }', 53 | [ 54 | 'validator' => [ 55 | 'key' => 'va l ue', 56 | 'foo' => 'bar', 57 | 'baz' => 'qux', 58 | ], 59 | ], 60 | ], 61 | 'options-with-double-quotes' => [ 62 | 'validator{key: "va l ue" , "foo" : " bar ", "baz": qux }', 63 | [ 64 | 'validator' => [ 65 | 'key' => 'va l ue', 66 | 'foo' => 'bar', 67 | 'baz' => 'qux', 68 | ], 69 | ], 70 | ], 71 | 'options-with-single-quotes' => [ 72 | "validator{key: 'va l ue' , 'foo' : ' bar ', 'baz': qux }", 73 | [ 74 | 'validator' => [ 75 | 'key' => 'va l ue', 76 | 'foo' => 'bar', 77 | 'baz' => 'qux', 78 | ], 79 | ], 80 | ], 81 | 'multiple-options-multiple-quotes-and-spaces' => [ 82 | 'validator{key: "va l ue" , \'foo\' : " bar ", \'baz\': q.u.x. }', 83 | [ 84 | 'validator' => [ 85 | 'key' => 'va l ue', 86 | 'foo' => 'bar', 87 | 'baz' => 'q.u.x.', 88 | ], 89 | ], 90 | ], 91 | 'multiple-options-and-validators' => [ 92 | 'stringlength{min:2,max:140}|validator{key:val,foo:bar}|notempty', 93 | [ 94 | 'stringlength' => [ 95 | 'min' => '2', 96 | 'max' => '140', 97 | ], 98 | 'validator' => [ 99 | 'key' => 'val', 100 | 'foo' => 'bar', 101 | ], 102 | 'notempty' => [], 103 | ], 104 | ], 105 | 'invalid-filter' => [ 106 | 'invalid filter ++', 107 | [ 108 | 'invalid' => [], 109 | ], 110 | ], 111 | 'empty-filter' => [ 112 | '', 113 | [], 114 | ], 115 | ]; 116 | } 117 | 118 | /** 119 | * @dataProvider dataAttributesProvider 120 | */ 121 | public function testParseDataAttribute(string $dataAttribute, array $expected): void 122 | { 123 | $reflectionMethod = new ReflectionMethod(Text::class, 'parseDataAttribute'); 124 | $reflectionMethod->setAccessible(true); 125 | $actual = iterator_to_array($reflectionMethod->invokeArgs( 126 | new Text( 127 | $this->prophesize(DOMElement::class)->reveal(), 128 | $this->prophesize(DOMDocument::class)->reveal() 129 | ), 130 | [$dataAttribute] 131 | )); 132 | 133 | self::assertEquals($expected, $actual); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /test/FormElementArraytest.php: -------------------------------------------------------------------------------- 1 | 28 | 29 | 30 | 31 | '; 32 | 33 | $form = (new FormFactory())->fromHtml($html); 34 | $request = $this->prophesize(ServerRequestInterface::class); 35 | $request->getMethod()->willReturn('POST'); 36 | $request->getParsedBody()->willReturn([ 37 | 'cars' => ['audi', 'bmw'], 38 | ]); 39 | 40 | $result = $form->validateRequest($request->reveal()); 41 | 42 | self::assertInstanceOf(ValidationResult::class, $result); 43 | self::assertEquals(['cars' => ['audi', 'bmw']], $result->getValues()); 44 | self::assertEquals([], $result->getMessages()); 45 | self::assertTrue($result->isValid()); 46 | 47 | $expected = ' 48 |
49 | 50 | 51 | 52 |
'; 53 | 54 | $actual = $form->asString($result); 55 | self::assertEquals( 56 | $this->getDomDocument($expected), 57 | $this->getDomDocument($actual), 58 | 'Failed asserting that the form is rendered correctly.' 59 | ); 60 | } 61 | 62 | private function getDomDocument(string $html): string 63 | { 64 | $doc = new DOMDocument('1.0', 'utf-8'); 65 | 66 | $doc->preserveWhiteSpace = false; 67 | // Don't add missing doctype, html and body 68 | //libxml_use_internal_errors(true); 69 | $doc->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD | LIBXML_NOBLANKS); 70 | //libxml_use_internal_errors(false); 71 | // Remove whitespace for better comparison 72 | 73 | return preg_replace('~\s+~i', ' ', $doc->saveHTML()); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/FormElementsTest.php: -------------------------------------------------------------------------------- 1 | expectException($expectedException); 53 | } 54 | 55 | $form = (new FormFactory())->fromHtml($htmlForm, $defaultValues); 56 | $result = $form->validate($submittedValues); 57 | 58 | self::assertInstanceOf(ValidationResult::class, $result); 59 | self::assertEquals( 60 | $submittedValues, 61 | $result->getRawValues(), 62 | 'Failed asserting submitted values are equal.' 63 | ); 64 | self::assertEquals($expectedValues, $result->getValues(), 'Failed asserting filtered values are equal.'); 65 | 66 | if ($expectedForm) { 67 | self::assertEqualForms($expectedForm, $form->asString($result)); 68 | } 69 | 70 | if (count($expectedErrors) === 0 && count($result->getMessages()) === 0) { 71 | self::assertTrue($result->isValid(), 'Failed asserting the validation result is valid.'); 72 | } else { 73 | self::assertFalse($result->isValid(), 'Failed asserting the validation result is invalid.'); 74 | } 75 | 76 | self::assertEmpty( 77 | $this->arrayDiff($expectedErrors, $result->getMessages()), 78 | 'Failed asserting that messages are equal.' 79 | ); 80 | } 81 | 82 | private function arrayDiff(array $array1, array $array2): array 83 | { 84 | $result = []; 85 | 86 | if ($res = array_merge(array_diff_key($array1, $array2), array_diff_key($array2, $array1))) { 87 | $result = $res; 88 | } 89 | 90 | foreach ($array1 as $key => $val) { 91 | if (! is_array($val)) { 92 | continue; 93 | } 94 | 95 | if (! array_key_exists($key, $array2)) { 96 | $result[$key] = $val; 97 | continue; 98 | } 99 | 100 | if ($res = array_merge(array_diff_key($val, $array2[$key]), array_diff_key($array2[$key], $val))) { 101 | $result[$key] = $res; 102 | } elseif ($res = $this->arrayDiff($val, $array2[$key])) { 103 | $result[$key] = $res; 104 | } 105 | } 106 | 107 | return $result; 108 | } 109 | 110 | public function getIntegrationTests(): Generator 111 | { 112 | $fixturesDir = realpath(__DIR__ . '/Fixtures/'); 113 | 114 | $rdi = new RecursiveDirectoryIterator($fixturesDir); 115 | $rii = new RecursiveIteratorIterator($rdi, RecursiveIteratorIterator::LEAVES_ONLY); 116 | foreach ($rii as $name => $file) { 117 | if (! preg_match('/\.test$/', $name)) { 118 | continue; 119 | } 120 | 121 | $testData = $this->readTestFile($file, $fixturesDir); 122 | 123 | $defaultValues = []; 124 | $submittedValues = []; 125 | $expectedValues = []; 126 | $expectedForm = ''; 127 | $expectedErrors = []; 128 | $expectedException = ''; 129 | 130 | try { 131 | $htmlForm = $testData['HTML-FORM']; 132 | 133 | if (! empty($testData['DEFAULT-VALUES'])) { 134 | $defaultValues = json_decode($testData['DEFAULT-VALUES'], true); 135 | } 136 | 137 | if (! empty($testData['SUBMITTED-VALUES'])) { 138 | $submittedValues = json_decode($testData['SUBMITTED-VALUES'], true); 139 | } 140 | 141 | if (! empty($testData['EXPECTED-VALUES'])) { 142 | $expectedValues = json_decode($testData['EXPECTED-VALUES'], true); 143 | } 144 | 145 | if (! empty($testData['EXPECTED-FORM'])) { 146 | $expectedForm = $testData['EXPECTED-FORM']; 147 | } 148 | 149 | if (! empty($testData['EXPECTED-ERRORS'])) { 150 | $expectedErrors = json_decode($testData['EXPECTED-ERRORS'], true); 151 | } 152 | 153 | if (! empty($testData['EXPECTED-EXCEPTION'])) { 154 | $expectedException = trim($testData['EXPECTED-EXCEPTION']); 155 | } 156 | } catch (Exception $e) { 157 | die(sprintf('Test "%s" is not valid: ' . $e->getMessage(), str_replace($fixturesDir . '/', '', $file))); 158 | } 159 | 160 | yield basename($file->getRealPath()) => [ 161 | $htmlForm, 162 | $defaultValues, 163 | $submittedValues, 164 | $expectedValues, 165 | $expectedForm, 166 | $expectedErrors, 167 | $expectedException, 168 | ]; 169 | } 170 | } 171 | 172 | protected function readTestFile(SplFileInfo $file, string $fixturesDir): array 173 | { 174 | $tokens = preg_split( 175 | '#(?:^|\n*)--([A-Z-]+)--\n#', 176 | file_get_contents($file->getRealPath()), 177 | -1, 178 | PREG_SPLIT_DELIM_CAPTURE 179 | ); 180 | 181 | $sectionInfo = [ 182 | 'TEST' => true, 183 | 'HTML-FORM' => true, 184 | 'DEFAULT-VALUES' => false, 185 | 'SUBMITTED-VALUES' => false, 186 | 'EXPECTED-VALUES' => false, 187 | 'EXPECTED-FORM' => false, 188 | 'EXPECTED-ERRORS' => false, 189 | 'EXPECTED-EXCEPTION' => false, 190 | ]; 191 | 192 | $data = []; 193 | $section = null; 194 | foreach ($tokens as $i => $token) { 195 | if (null === $section && ! $token) { 196 | continue; // skip leading blank 197 | } 198 | 199 | if (null === $section) { 200 | if (! array_key_exists($token, $sectionInfo)) { 201 | throw new RuntimeException(sprintf( 202 | 'The test file "%s" must not contain a section named "%s".', 203 | str_replace($fixturesDir . '/', '', $file), 204 | $token 205 | )); 206 | } 207 | $section = $token; 208 | continue; 209 | } 210 | 211 | $data[$section] = $token; 212 | $section = null; 213 | } 214 | 215 | foreach ($sectionInfo as $section => $required) { 216 | if ($required && ! array_key_exists($section, $data)) { 217 | throw new RuntimeException(sprintf( 218 | 'The test file "%s" must have a section named "%s".', 219 | str_replace($fixturesDir . '/', '', $file), 220 | $section 221 | )); 222 | } 223 | } 224 | 225 | return $data; 226 | } 227 | 228 | private function assertEqualForms(string $expected, string $actual): void 229 | { 230 | self::assertEquals( 231 | $this->getDomDocument($expected), 232 | $this->getDomDocument($actual), 233 | 'Failed asserting that the form is rendered correctly.' 234 | ); 235 | } 236 | 237 | private function getDomDocument(string $html): string 238 | { 239 | $doc = new DOMDocument('1.0', 'utf-8'); 240 | 241 | $doc->preserveWhiteSpace = false; 242 | 243 | // Don't add missing doctype, html and body 244 | //libxml_use_internal_errors(true); 245 | $doc->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD | LIBXML_NOBLANKS); 246 | //libxml_use_internal_errors(false); 247 | 248 | // Remove whitespace for better comparison 249 | return preg_replace('~\s+~i', ' ', $doc->saveHTML()); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /test/FormFactoryFactoryTest.php: -------------------------------------------------------------------------------- 1 | container = $this->prophesize(ContainerInterface::class); 31 | $this->inputFilterFactory = $this->prophesize(Factory::class); 32 | $this->formFactoryFactory = new FormFactoryFactory(); 33 | } 34 | 35 | public function testEmptyWithEmptyContainer(): void 36 | { 37 | $this->container->has(Factory::class)->willReturn(false); 38 | $this->container->get('config')->willReturn([]); 39 | 40 | $formFactory = ($this->formFactoryFactory)($this->container->reveal()); 41 | 42 | self::assertInstanceOf(FormFactoryInterface::class, $formFactory); 43 | } 44 | 45 | public function testContainerWithFactory(): void 46 | { 47 | $this->container->has(Factory::class)->willReturn(true); 48 | $this->container->get(Factory::class)->will([$this->inputFilterFactory, 'reveal'])->shouldBeCalled(); 49 | $this->container->get('config')->willReturn([]); 50 | 51 | $formFactory = ($this->formFactoryFactory)($this->container->reveal()); 52 | 53 | self::assertInstanceOf(FormFactoryInterface::class, $formFactory); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/FormFactoryTest.php: -------------------------------------------------------------------------------- 1 | prophesize(Factory::class); 32 | $options = ['foo' => 'bar']; 33 | 34 | $formFactory = new FormFactory($factory->reveal(), $options); 35 | 36 | self::assertInstanceOf(FormFactoryInterface::class, $formFactory); 37 | self::assertInstanceOf(FormFactory::class, $formFactory); 38 | } 39 | 40 | public function testCreatingFormFromHtml(): void 41 | { 42 | $html = ' 43 |
44 | 45 | 46 |
'; 47 | 48 | $formFactory = new FormFactory(); 49 | $form = $formFactory->fromHtml($html); 50 | 51 | self::assertInstanceOf(FormInterface::class, $form); 52 | self::assertInstanceOf(Form::class, $form); 53 | } 54 | 55 | public function testCreatingFormUsesInjectedFactoryAndOptions(): void 56 | { 57 | $html = ' 58 |
59 | 60 | 61 |
'; 62 | 63 | $factory = $this->prophesize(Factory::class); 64 | $inputFilter = $this->prophesize(InputFilterInterface::class); 65 | $options = [ 66 | 'cssHasErrorClass' => 'has-error', 67 | 'cssErrorClass' => 'error', 68 | ]; 69 | $defaults = ['foo' => 'bar']; 70 | 71 | $propFactory = new ReflectionProperty(Form::class, 'factory'); 72 | $propFactory->setAccessible(true); 73 | 74 | $propInputFilter = new ReflectionProperty(Form::class, 'inputFilter'); 75 | $propInputFilter->setAccessible(true); 76 | 77 | $propCssHasErrorClass = new ReflectionProperty(Form::class, 'cssHasErrorClass'); 78 | $propCssHasErrorClass->setAccessible(true); 79 | 80 | $propCssErrorClass = new ReflectionProperty(Form::class, 'cssErrorClass'); 81 | $propCssErrorClass->setAccessible(true); 82 | 83 | $formFactory = new FormFactory($factory->reveal(), $options); 84 | $form = $formFactory->fromHtml($html, $defaults, $inputFilter->reveal()); 85 | 86 | self::assertInstanceOf(FormInterface::class, $form); 87 | self::assertInstanceOf(Form::class, $form); 88 | self::assertSame($factory->reveal(), $propFactory->getValue($form)); 89 | self::assertSame($inputFilter->reveal(), $propInputFilter->getValue($form)); 90 | self::assertSame($options['cssHasErrorClass'], $propCssHasErrorClass->getValue($form)); 91 | self::assertSame($options['cssErrorClass'], $propCssErrorClass->getValue($form)); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/FormTest.php: -------------------------------------------------------------------------------- 1 | 'bar', 20 | 'baz' => ' qux ', 21 | ]; 22 | 23 | /** @var string[] */ 24 | private $values = [ 25 | 'foo' => 'bar', 26 | 'baz' => 'qux', 27 | ]; 28 | 29 | /** @var string[] */ 30 | private $messages = [ 31 | 'foo' => ['regexNotMatch' => 'The input does not match against pattern \'/^\d+$/\''], 32 | ]; 33 | 34 | public function testPsrPostRequestIsValid(): void 35 | { 36 | $html = ' 37 |
38 | 39 | 40 |
'; 41 | 42 | $form = (new FormFactory())->fromHtml($html); 43 | $request = $this->prophesize(ServerRequestInterface::class); 44 | $request->getMethod()->willReturn('POST'); 45 | $request->getParsedBody()->willReturn($this->rawValues); 46 | 47 | $result = $form->validateRequest($request->reveal()); 48 | 49 | self::assertInstanceOf(ValidationResult::class, $result); 50 | self::assertEquals($this->rawValues, $result->getRawValues()); 51 | self::assertEquals($this->values, $result->getValues()); 52 | self::assertEquals([], $result->getMessages()); 53 | self::assertTrue($result->isValid()); 54 | } 55 | 56 | public function testPsrGetRequestIsNotValid(): void 57 | { 58 | $html = ' 59 |
60 | 61 | 62 |
'; 63 | 64 | $form = (new FormFactory())->fromHtml($html); 65 | $request = $this->prophesize(ServerRequestInterface::class); 66 | $request->getMethod()->willReturn('GET'); 67 | $request->getParsedBody()->willReturn($this->rawValues); 68 | 69 | $result = $form->validateRequest($request->reveal()); 70 | 71 | self::assertInstanceOf(ValidationResult::class, $result); 72 | self::assertEquals([], $result->getRawValues()); 73 | self::assertEquals([], $result->getValues()); 74 | self::assertEquals([], $result->getMessages()); 75 | self::assertFalse($result->isValid()); 76 | } 77 | 78 | public function testPsrPostRequestHasMessages(): void 79 | { 80 | $html = ' 81 |
82 | 83 | 84 |
'; 85 | 86 | $form = (new FormFactory())->fromHtml($html); 87 | $request = $this->prophesize(ServerRequestInterface::class); 88 | $request->getMethod()->willReturn('POST'); 89 | $request->getParsedBody()->willReturn($this->rawValues); 90 | 91 | $result = $form->validateRequest($request->reveal()); 92 | 93 | self::assertInstanceOf(ValidationResult::class, $result); 94 | self::assertEquals($this->rawValues, $result->getRawValues()); 95 | self::assertEquals($this->values, $result->getValues()); 96 | self::assertEquals($this->messages, $result->getMessages()); 97 | self::assertFalse($result->isValid()); 98 | } 99 | 100 | public function testSetValuesStatically(): void 101 | { 102 | $html = ' 103 |
104 | 105 | 106 |
'; 107 | 108 | $form = (new FormFactory())->fromHtml($html, [ 109 | 'foo' => 'bar', 110 | 'baz' => 'qux', 111 | ]); 112 | 113 | self::assertStringContainsString('', $form->asString()); 114 | self::assertStringContainsString('', $form->asString()); 115 | } 116 | 117 | public function testSetValuesWithConstructor(): void 118 | { 119 | $html = ' 120 |
121 | 122 | 123 |
'; 124 | 125 | $form = (new FormFactory())->fromHtml($html, [ 126 | 'foo' => 'bar', 127 | 'baz' => 'qux', 128 | ]); 129 | 130 | self::assertStringContainsString('', $form->asString()); 131 | self::assertStringContainsString('', $form->asString()); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /test/InputFilterFactoryTest.php: -------------------------------------------------------------------------------- 1 | [ 22 | 'validators' => [ 23 | // Attach custom validators or override standard validators 24 | 'invokables' => [ 25 | 'recaptcha' => RecaptchaValidator::class, 26 | ], 27 | ], 28 | 'filters' => [ 29 | // Attach custom filters or override standard filters 30 | 'invokables' => [], 31 | ], 32 | ], 33 | ]; 34 | } 35 | 36 | // Build container 37 | $container = new ServiceManager($config); 38 | $container->setService('config', $config); 39 | 40 | $factory = new InputFilterFactory(); 41 | 42 | return $factory($container); 43 | } 44 | 45 | public function testInputFilterFactoryCreation(): void 46 | { 47 | $factory = $this->createFactory(false); 48 | 49 | self::assertInstanceOf(Factory::class, $factory); 50 | self::assertFalse($factory->getDefaultValidatorChain()->getPluginManager()->has('recaptcha')); 51 | } 52 | 53 | public function testCustomValidatorIsRegistered(): void 54 | { 55 | $factory = $this->createFactory(true); 56 | 57 | self::assertTrue($factory->getDefaultValidatorChain()->getPluginManager()->has('recaptcha')); 58 | self::assertInstanceOf( 59 | RecaptchaValidator::class, 60 | $factory->getDefaultValidatorChain()->getPluginManager()->get('recaptcha', ['key' => 'secret_key']) 61 | ); 62 | } 63 | 64 | public function testInputHasAccessToCustomValidator(): void 65 | { 66 | $factory = $this->createFactory(true); 67 | 68 | $input = $factory->createInput(['name' => 'foo']); 69 | 70 | self::assertTrue($input->getValidatorChain()->getPluginManager()->has('recaptcha')); 71 | self::assertInstanceOf( 72 | RecaptchaValidator::class, 73 | $input->getValidatorChain()->getPluginManager()->get('recaptcha', ['key' => 'secret_key']) 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/ValidationResultTest.php: -------------------------------------------------------------------------------- 1 | 'bar', 15 | 'baz' => ' qux ', 16 | ]; 17 | 18 | /** @var string[] */ 19 | private $values = [ 20 | 'foo' => 'bar', 21 | 'baz' => 'qux', 22 | ]; 23 | 24 | /** @var string[][] */ 25 | private $messages = [ 26 | 'foo' => [ 27 | 'regexNotMatch' => '', 28 | 'notInArray' => '', 29 | ], 30 | ]; 31 | 32 | public function testValuesAreAvailable(): void 33 | { 34 | $result = new ValidationResult($this->rawValues, $this->values, $this->messages, null); 35 | 36 | self::assertEquals($this->rawValues, $result->getRawValues()); 37 | self::assertEquals($this->values, $result->getValues()); 38 | self::assertEquals($this->messages, $result->getMessages()); 39 | } 40 | 41 | public function testResultIsValid(): void 42 | { 43 | $result = new ValidationResult($this->rawValues, $this->values, [], null); 44 | 45 | self::assertTrue($result->isValid()); 46 | } 47 | 48 | public function testPostResultIsValid(): void 49 | { 50 | $result = new ValidationResult($this->rawValues, $this->values, [], 'POST'); 51 | 52 | self::assertTrue($result->isValid()); 53 | } 54 | 55 | public function testResultWithMessagesIsNotValid(): void 56 | { 57 | $result = new ValidationResult($this->rawValues, $this->values, $this->messages, null); 58 | 59 | self::assertFalse($result->isValid()); 60 | } 61 | 62 | public function testPostResultWithMessagesIsNotValid(): void 63 | { 64 | $result = new ValidationResult($this->rawValues, $this->values, $this->messages, 'POST'); 65 | 66 | self::assertFalse($result->isValid()); 67 | } 68 | 69 | public function testGetResultIsNotValid(): void 70 | { 71 | $result = new ValidationResult($this->rawValues, $this->values, $this->messages, 'GET'); 72 | 73 | self::assertFalse($result->isValid()); 74 | } 75 | 76 | public function testSubmitButtonIsClicked(): void 77 | { 78 | $result = new ValidationResult($this->rawValues, $this->values, $this->messages, 'POST', 'confirm'); 79 | 80 | self::assertTrue($result->isClicked('confirm')); 81 | self::assertFalse($result->isClicked('cancel')); 82 | self::assertEquals('confirm', $result->getClicked()); 83 | } 84 | 85 | public function testSubmitButtonIsNotClicked(): void 86 | { 87 | $result = new ValidationResult($this->rawValues, $this->values, $this->messages, 'POST', null); 88 | 89 | self::assertFalse($result->isClicked('confirm')); 90 | self::assertFalse($result->isClicked('cancel')); 91 | self::assertNull($result->getClicked()); 92 | } 93 | 94 | public function testMessagesCanBeAdded(): void 95 | { 96 | $result = new ValidationResult($this->rawValues, $this->values, $this->messages, 'POST'); 97 | 98 | $result->addMessages([ 99 | 'foo' => [ 100 | 'invalidUuid' => 'This is not a valid uuid', 101 | 'notInArray' => 'This is not in array', 102 | ], 103 | 'baz' => [ 104 | 'isRequired' => 'This is required', 105 | ], 106 | ]); 107 | 108 | $expected = [ 109 | 'foo' => [ 110 | 'regexNotMatch' => '', 111 | 'invalidUuid' => 'This is not a valid uuid', 112 | 'notInArray' => 'This is not in array', 113 | ], 114 | 'baz' => [ 115 | 'isRequired' => 'This is required', 116 | ], 117 | ]; 118 | 119 | self::assertEquals($expected, $result->getMessages()); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /test/Validator/RecaptchaValidatorTest.php: -------------------------------------------------------------------------------- 1 | validator = new Validator\RecaptchaValidator(['key' => $this->privKey]); 30 | } 31 | 32 | public function testOptionsIteratorToArray(): void 33 | { 34 | $options = ['key' => $this->privKey]; 35 | $iterator = new ArrayIterator($options); 36 | $this->validator = new Validator\RecaptchaValidator($iterator); 37 | 38 | $reflectionClass = new ReflectionClass(Validator\RecaptchaValidator::class); 39 | 40 | $reflectionProperty = $reflectionClass->getProperty('options'); 41 | $reflectionProperty->setAccessible(true); 42 | $actualOptions = $reflectionProperty->getValue($this->validator); 43 | 44 | self::assertEquals($options, $actualOptions); 45 | } 46 | 47 | public function testMissingKeyOptionThrowsInvalidArgumentException(): void 48 | { 49 | $this->expectException(InvalidArgumentException::class); 50 | 51 | new Validator\RecaptchaValidator(); 52 | } 53 | 54 | public function testGetMessages(): void 55 | { 56 | self::assertEquals([], $this->validator->getMessages()); 57 | } 58 | } 59 | --------------------------------------------------------------------------------