diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47c3096..cbe6308 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,9 @@ on: [push] jobs: composer: runs-on: ubuntu-latest + strategy: + matrix: + php: [ 8.1, 8.2 ] steps: - uses: actions/checkout@v3 @@ -18,7 +21,7 @@ jobs: - name: Composer install uses: php-actions/composer@v6 with: - php_version: 8.1 + php_version: ${{ matrix.php }} - name: Archive build run: mkdir /tmp/github-actions/ && tar -cvf /tmp/github-actions/build.tar ./ @@ -31,7 +34,10 @@ jobs: phpunit: runs-on: ubuntu-latest - needs: [composer] + needs: [ composer ] + strategy: + matrix: + php: [ 8.1, 8.2 ] outputs: coverage: ${{ steps.store-coverage.outputs.coverage_text }} @@ -50,7 +56,7 @@ jobs: env: XDEBUG_MODE: cover with: - php_version: 8.1 + php_version: ${{ matrix.php }} php_extensions: xdebug configuration: test/phpunit/phpunit.xml bootstrap: vendor/autoload.php @@ -83,7 +89,10 @@ jobs: phpstan: runs-on: ubuntu-latest - needs: [composer] + needs: [ composer ] + strategy: + matrix: + php: [ 8.1, 8.2 ] steps: - uses: actions/download-artifact@v3 @@ -97,8 +106,56 @@ jobs: - name: PHP Static Analysis uses: php-actions/phpstan@v3 with: + php_version: ${{ matrix.php }} path: src/ + phpmd: + runs-on: ubuntu-latest + needs: [ composer ] + strategy: + matrix: + php: [ 8.1, 8.2 ] + + steps: + - uses: actions/download-artifact@v3 + with: + name: build-artifact + path: /tmp/github-actions + + - name: Extract build archive + run: tar -xvf /tmp/github-actions/build.tar ./ + + - name: PHP Mess Detector + uses: php-actions/phpmd@v1 + with: + php_version: ${{ matrix.php }} + path: src/ + output: text + ruleset: phpmd.xml + + phpcs: + runs-on: ubuntu-latest + needs: [ composer ] + strategy: + matrix: + php: [ 8.1, 8.2 ] + + steps: + - uses: actions/download-artifact@v3 + with: + name: build-artifact + path: /tmp/github-actions + + - name: Extract build archive + run: tar -xvf /tmp/github-actions/build.tar ./ + + - name: PHP Code Sniffer + uses: php-actions/phpcs@v1 + with: + php_version: ${{ matrix.php }} + path: src/ + standard: phpcs.xml + remove_old_artifacts: runs-on: ubuntu-latest diff --git a/README.md b/README.md index 0b6142f..e598af3 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This repository performs W3C form validation for projects that have a [server-si Build status - + Code quality diff --git a/composer.json b/composer.json index f2cfb32..bd16d36 100644 --- a/composer.json +++ b/composer.json @@ -15,8 +15,10 @@ }, "require-dev": { - "phpunit/phpunit": "^10", - "phpstan/phpstan": "^1.7.2" + "phpunit/phpunit": "^10.0", + "phpstan/phpstan": "^1.10", + "phpmd/phpmd": "^2.13", + "squizlabs/php_codesniffer": "^3.7" }, "autoload": { diff --git a/composer.lock b/composer.lock index 81aea35..6b8c8a4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "33f917d4399beea20a2f8ca021a2b11e", + "content-hash": "be033f6c0674633bc8684f9c34f589d7", "packages": [ { "name": "phpgt/cssxpath", @@ -62,16 +62,16 @@ }, { "name": "phpgt/dom", - "version": "v4.1.1", + "version": "v4.1.2", "source": { "type": "git", "url": "https://github.com/PhpGt/Dom.git", - "reference": "19ad48319273f725bcbfb9b2784cdf1deb1cf27f" + "reference": "3e1afe7b6d38e9968d400900a62cbe54753973a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PhpGt/Dom/zipball/19ad48319273f725bcbfb9b2784cdf1deb1cf27f", - "reference": "19ad48319273f725bcbfb9b2784cdf1deb1cf27f", + "url": "https://api.github.com/repos/PhpGt/Dom/zipball/3e1afe7b6d38e9968d400900a62cbe54753973a1", + "reference": "3e1afe7b6d38e9968d400900a62cbe54753973a1", "shasum": "" }, "require": { @@ -79,13 +79,15 @@ "ext-libxml": "*", "ext-mbstring": "*", "php": ">=8.1", - "phpgt/cssxpath": "^v1.1", - "phpgt/propfunc": "^v1.0", - "psr/http-message": "^v1.0" + "phpgt/cssxpath": "^1.1", + "phpgt/propfunc": "^1.0", + "psr/http-message": "^1.0" }, "require-dev": { - "phpstan/phpstan": "v1.8", - "phpunit/phpunit": "v9.5" + "phpmd/phpmd": "^2.13", + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^10.0", + "squizlabs/php_codesniffer": "^3.7" }, "type": "library", "autoload": { @@ -148,7 +150,7 @@ "description": "The modern DOM API for PHP projects.", "support": { "issues": "https://github.com/PhpGt/Dom/issues", - "source": "https://github.com/PhpGt/Dom/tree/v4.1.1" + "source": "https://github.com/PhpGt/Dom/tree/v4.1.2" }, "funding": [ { @@ -156,7 +158,7 @@ "type": "github" } ], - "time": "2022-09-25T10:57:14+00:00" + "time": "2023-03-02T18:40:07+00:00" }, { "name": "phpgt/propfunc", @@ -265,6 +267,143 @@ } ], "packages-dev": [ + { + "name": "composer/pcre", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", + "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.1.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-11-17T09:50:14+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "ced299686f41dce890debac69273b47ffe98a40c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", + "reference": "ced299686f41dce890debac69273b47ffe98a40c", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-02-25T21:32:43+00:00" + }, { "name": "myclabs/deep-copy", "version": "1.11.0", @@ -380,6 +519,63 @@ }, "time": "2023-01-16T22:05:37+00:00" }, + { + "name": "pdepend/pdepend", + "version": "2.13.0", + "source": { + "type": "git", + "url": "https://github.com/pdepend/pdepend.git", + "reference": "31be7cd4f305f3f7b52af99c1cb13fc938d1cfad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pdepend/pdepend/zipball/31be7cd4f305f3f7b52af99c1cb13fc938d1cfad", + "reference": "31be7cd4f305f3f7b52af99c1cb13fc938d1cfad", + "shasum": "" + }, + "require": { + "php": ">=5.3.7", + "symfony/config": "^2.3.0|^3|^4|^5|^6.0", + "symfony/dependency-injection": "^2.3.0|^3|^4|^5|^6.0", + "symfony/filesystem": "^2.3.0|^3|^4|^5|^6.0" + }, + "require-dev": { + "easy-doc/easy-doc": "0.0.0|^1.2.3", + "gregwar/rst": "^1.0", + "phpunit/phpunit": "^4.8.36|^5.7.27", + "squizlabs/php_codesniffer": "^2.0.0" + }, + "bin": [ + "src/bin/pdepend" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "PDepend\\": "src/main/php/PDepend" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Official version of pdepend to be handled with Composer", + "support": { + "issues": "https://github.com/pdepend/pdepend/issues", + "source": "https://github.com/pdepend/pdepend/tree/2.13.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/pdepend/pdepend", + "type": "tidelift" + } + ], + "time": "2023-02-28T20:56:15+00:00" + }, { "name": "phar-io/manifest", "version": "2.0.3", @@ -491,18 +687,101 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "phpmd/phpmd", + "version": "2.13.0", + "source": { + "type": "git", + "url": "https://github.com/phpmd/phpmd.git", + "reference": "dad0228156856b3ad959992f9748514fa943f3e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpmd/phpmd/zipball/dad0228156856b3ad959992f9748514fa943f3e3", + "reference": "dad0228156856b3ad959992f9748514fa943f3e3", + "shasum": "" + }, + "require": { + "composer/xdebug-handler": "^1.0 || ^2.0 || ^3.0", + "ext-xml": "*", + "pdepend/pdepend": "^2.12.1", + "php": ">=5.3.9" + }, + "require-dev": { + "easy-doc/easy-doc": "0.0.0 || ^1.3.2", + "ext-json": "*", + "ext-simplexml": "*", + "gregwar/rst": "^1.0", + "mikey179/vfsstream": "^1.6.8", + "phpunit/phpunit": "^4.8.36 || ^5.7.27", + "squizlabs/php_codesniffer": "^2.0" + }, + "bin": [ + "src/bin/phpmd" + ], + "type": "library", + "autoload": { + "psr-0": { + "PHPMD\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Manuel Pichler", + "email": "github@manuel-pichler.de", + "homepage": "https://github.com/manuelpichler", + "role": "Project Founder" + }, + { + "name": "Marc Würth", + "email": "ravage@bluewin.ch", + "homepage": "https://github.com/ravage84", + "role": "Project Maintainer" + }, + { + "name": "Other contributors", + "homepage": "https://github.com/phpmd/phpmd/graphs/contributors", + "role": "Contributors" + } + ], + "description": "PHPMD is a spin-off project of PHP Depend and aims to be a PHP equivalent of the well known Java tool PMD.", + "homepage": "https://phpmd.org/", + "keywords": [ + "mess detection", + "mess detector", + "pdepend", + "phpmd", + "pmd" + ], + "support": { + "irc": "irc://irc.freenode.org/phpmd", + "issues": "https://github.com/phpmd/phpmd/issues", + "source": "https://github.com/phpmd/phpmd/tree/2.13.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/phpmd/phpmd", + "type": "tidelift" + } + ], + "time": "2022-09-10T08:44:15+00:00" + }, { "name": "phpstan/phpstan", - "version": "1.9.17", + "version": "1.10.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "204e459e7822f2c586463029f5ecec31bb45a1f2" + "reference": "5419375b5891add97dc74be71e6c1c34baaddf64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/204e459e7822f2c586463029f5ecec31bb45a1f2", - "reference": "204e459e7822f2c586463029f5ecec31bb45a1f2", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/5419375b5891add97dc74be71e6c1c34baaddf64", + "reference": "5419375b5891add97dc74be71e6c1c34baaddf64", "shasum": "" }, "require": { @@ -532,7 +811,7 @@ ], "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.9.17" + "source": "https://github.com/phpstan/phpstan/tree/1.10.3" }, "funding": [ { @@ -548,27 +827,27 @@ "type": "tidelift" } ], - "time": "2023-02-08T12:25:00+00:00" + "time": "2023-02-25T14:47:13+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "10.0.0", + "version": "10.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "bf4fbc9c13af7da12b3ea807574fb460f255daba" + "reference": "b9c21a93dd8c8eed79879374884ee733259475cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/bf4fbc9c13af7da12b3ea807574fb460f255daba", - "reference": "bf4fbc9c13af7da12b3ea807574fb460f255daba", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b9c21a93dd8c8eed79879374884ee733259475cc", + "reference": "b9c21a93dd8c8eed79879374884ee733259475cc", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.14", + "nikic/php-parser": "^4.15", "php": ">=8.1", "phpunit/php-file-iterator": "^4.0", "phpunit/php-text-template": "^3.0", @@ -617,7 +896,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.0.0" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.0.1" }, "funding": [ { @@ -625,7 +904,7 @@ "type": "github" } ], - "time": "2023-02-03T07:14:34+00:00" + "time": "2023-02-25T05:35:03+00:00" }, { "name": "phpunit/php-file-iterator", @@ -870,16 +1149,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.0.7", + "version": "10.0.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a6f61c5629dd95db79af72f1e94d56702187479a" + "reference": "7065dbebcb0f66cf16a45fc9cfc28c2351e06169" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a6f61c5629dd95db79af72f1e94d56702187479a", - "reference": "a6f61c5629dd95db79af72f1e94d56702187479a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/7065dbebcb0f66cf16a45fc9cfc28c2351e06169", + "reference": "7065dbebcb0f66cf16a45fc9cfc28c2351e06169", "shasum": "" }, "require": { @@ -950,7 +1229,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.0.7" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.0.14" }, "funding": [ { @@ -966,7 +1245,110 @@ "type": "tidelift" } ], - "time": "2023-02-08T15:16:31+00:00" + "time": "2023-03-01T05:37:49+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/log", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.0" + }, + "time": "2021-07-14T16:46:02+00:00" }, { "name": "sebastian/cli-parser", @@ -1876,6 +2258,681 @@ ], "time": "2023-02-07T11:34:05+00:00" }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.7.2", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + }, + "time": "2023-02-22T23:07:41+00:00" + }, + { + "name": "symfony/config", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "249271da6f545d6579e0663374f8249a80be2893" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/249271da6f545d6579e0663374f8249a80be2893", + "reference": "249271da6f545d6579e0663374f8249a80be2893", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/filesystem": "^5.4|^6.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<5.4" + }, + "require-dev": { + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/messenger": "^5.4|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/yaml": "^5.4|^6.0" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-14T08:44:56+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "83369dd4ec84bba9673524d25b79dfbde9e6e84c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/83369dd4ec84bba9673524d25b79dfbde9e6e84c", + "reference": "83369dd4ec84bba9673524d25b79dfbde9e6e84c", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/service-contracts": "^1.1.6|^2.0|^3.0", + "symfony/var-exporter": "^6.2.7" + }, + "conflict": { + "ext-psr": "<1.1|>=2", + "symfony/config": "<6.1", + "symfony/finder": "<5.4", + "symfony/proxy-manager-bridge": "<6.2", + "symfony/yaml": "<5.4" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "symfony/config": "^6.1", + "symfony/expression-language": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0" + }, + "suggest": { + "symfony/config": "", + "symfony/expression-language": "For using expressions in service container configuration", + "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required", + "symfony/yaml": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-16T14:11:02+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", + "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.3-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.2.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-01T10:25:55+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "82b6c62b959f642d000456f08c6d219d749215b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/82b6c62b959f642d000456f08c6d219d749215b3", + "reference": "82b6c62b959f642d000456f08c6d219d749215b3", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-14T08:44:56+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "a8c9cedf55f314f3a186041d19537303766df09a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/a8c9cedf55f314f3a186041d19537303766df09a", + "reference": "a8c9cedf55f314f3a186041d19537303766df09a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.3-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.2.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-01T10:32:47+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "86062dd0103530e151588c8f60f5b85a139f1442" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/86062dd0103530e151588c8f60f5b85a139f1442", + "reference": "86062dd0103530e151588c8f60f5b85a139f1442", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/var-dumper": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-24T10:42:00+00:00" + }, { "name": "theseer/tokenizer", "version": "1.2.1", diff --git a/example/01-shop.php b/example/01-shop.php new file mode 100644 index 0000000..b313d8b --- /dev/null +++ b/example/01-shop.php @@ -0,0 +1,82 @@ + + +
+ + + + + + +
+HTML; + +function example(HTMLDocument $document, array $input) { + $validator = new Validator(); + $form = $document->forms[0]; + + try { + $validator->validate($form, $input); + } catch(ValidationException $exception) { + foreach($validator->getLastErrorList() as $name => $message) { + $errorElement = $form->querySelector("[name=$name]"); + $errorElement->parentNode->dataset->validationError = $message; + } + return; + } + + echo "Payment succeeded!"; + exit; +} + +$document = new HTMLDocument($html); + +if(isset($_POST["do"]) && $_POST["do"] === "buy") { + example($document, $_POST); +} + +echo $document; diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..7eaee99 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,72 @@ + + + ./src + + Created from PHP.Gt/Styleguide + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + diff --git a/phpmd.xml b/phpmd.xml new file mode 100644 index 0000000..404bc5d --- /dev/null +++ b/phpmd.xml @@ -0,0 +1,63 @@ + + + Custom ruleset + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ErrorList.php b/src/ErrorList.php index 9157c6f..9111fd2 100644 --- a/src/ErrorList.php +++ b/src/ErrorList.php @@ -5,7 +5,7 @@ use Gt\Dom\Element; use Iterator; -/** @implements Iterator */ +/** @implements Iterator */ class ErrorList implements Countable, Iterator { /** @var array */ protected array $errorArray; @@ -38,9 +38,12 @@ public function valid():bool { return isset($keys[$this->iteratorKey]); } - public function current():array { + public function current():string { $keys = array_keys($this->errorArray); - return $this->errorArray[$keys[$this->iteratorKey]]; + return implode( + "; ", + array_unique($this->errorArray[$keys[$this->iteratorKey]] ?? []) + ); } public function next():void { diff --git a/src/Rule/Pattern.php b/src/Rule/Pattern.php index 6a25114..4971d4c 100644 --- a/src/Rule/Pattern.php +++ b/src/Rule/Pattern.php @@ -8,8 +8,12 @@ class Pattern extends Rule { "pattern", ]; - public function isValid(Element $element, string $value, array $inputKvp):bool { - $pattern = "/" . $element->getAttribute("pattern") . "/"; + public function isValid( + Element $element, + string $value, + array $inputKvp, + ):bool { + $pattern = "/" . $element->getAttribute("pattern") . "/u"; return (bool)preg_match($pattern, $value); } diff --git a/src/Rule/Rule.php b/src/Rule/Rule.php index e96d8cb..ce9f41c 100644 --- a/src/Rule/Rule.php +++ b/src/Rule/Rule.php @@ -19,7 +19,12 @@ public function getAttributes():array { return $this->attributes; } - abstract public function isValid(Element $element, string $value, array $inputKvp):bool; + /** @param array $inputKvp */ + abstract public function isValid( + Element $element, + string $value, + array $inputKvp, + ):bool; abstract public function getHint(Element $element, string $value):string; } diff --git a/src/Rule/TypeDate.php b/src/Rule/TypeDate.php index e71dfab..344eda6 100644 --- a/src/Rule/TypeDate.php +++ b/src/Rule/TypeDate.php @@ -20,63 +20,21 @@ class TypeDate extends Rule { "type=time", ]; - public function isValid(Element $element, string $value, array $inputKvp):bool { + public function isValid( + Element $element, + string $value, + array $inputKvp + ):bool { if($value === "") { return true; } - $dateTime = null; + $dateTime = $this->extractDateTime( + $value, + $element->getAttribute("type") + ); - switch($element->getAttribute("type")) { - case "date": - $dateTime = DateTime::createFromFormat( - self::FORMAT_DATE, - $value - ); - break; - - case "month": - $dateTime = DateTime::createFromFormat( - self::FORMAT_MONTH, - $value - ); - break; - - case "week": - $success = preg_match( - "/^(?P\d{4})-W(?P\d{1,2})$/", - $value, - $matches - ); - if(!$success) { - return false; - } - - if($matches["week"] < 1 || $matches["week"] > 52) { - return false; - } - - $dateTime = new DateTime(); - $dateTime->setISODate((int)$matches["year"], (int)$matches["week"]); - - break; - - case "datetime-local": - $dateTime = DateTime::createFromFormat( - self::FORMAT_DATETIME_LOCAL, - $value - ); - break; - - case "time": - $dateTime = DateTime::createFromFormat( - self::FORMAT_TIME, - $value - ); - break; - } - - return $dateTime !== false; + return !is_null($dateTime); } public function getHint(Element $element, string $value):string { @@ -107,4 +65,43 @@ public function getHint(Element $element, string $value):string { return "Field must be a $type in the format $format"; } + + private function extractDateTime( + string $value, + string $type, + ):?DateTime { + if($type === "week") { + return $this->extractDateTimeWeek($value); + } + else { + $format = match($type) { + "date" => self::FORMAT_DATE, + "month" => self::FORMAT_MONTH, + "datetime-local" => self::FORMAT_DATETIME_LOCAL, + "time" => self::FORMAT_TIME, + default => "", + }; + + return DateTime::createFromFormat($format, $value) ?: null; + } + } + + private function extractDateTimeWeek(string $value):?DateTime { + $success = preg_match( + "/^(?P\d{4})-W(?P\d{1,2})$/", + $value, + $matches + ); + if(!$success) { + return null; + } + + if($matches["week"] < 1 || $matches["week"] > 52) { + return null; + } + + $dateTime = new DateTime(); + $dateTime->setISODate((int)$matches["year"], (int)$matches["week"]); + return $dateTime; + } } diff --git a/src/Rule/TypeNumber.php b/src/Rule/TypeNumber.php index c7ecc65..ab622d6 100644 --- a/src/Rule/TypeNumber.php +++ b/src/Rule/TypeNumber.php @@ -10,88 +10,139 @@ class TypeNumber extends Rule { "type=range", ]; - public function isValid(Element $element, string $value, array $inputKvp):bool { - if($min = $element->getAttribute("min") ?: null) { - $min = (float)$min; - } - if($max = $element->getAttribute("max") ?: null) { - $max = (float)$max; - } - if($step = $element->getAttribute("step") ?: null) { - $step = (float)$step; - } - + public function isValid( + Element $element, + string $value, + array $inputKvp, + ):bool { if($value === "") { - $validity = true; + return true; } - elseif(is_numeric($value)) { + + if(is_numeric($value)) { $value = (float)$value; - if(!is_null($min) - && $value < $min) { - $validity = false; + if(false === $this->isValidMin( + $element->getAttribute("min"), + $value, + )) { + return false; } - elseif(!is_null($max) - && $value > $max) { - $validity = false; + if(false === $this->isValidMax( + $element->getAttribute("max"), + $value, + )) { + return false; } - elseif(!is_null($step)) { - if($min) { - $validity = ($value - $min) % $step === 0; - } - else { - $validity = $value % $step === 0; - } - - } - else { - $validity = true; + if(false === $this->isValidStep( + $element->getAttribute("min"), + $element->getAttribute("step"), + $value, + )) { + return false; } } else { - $validity = false; + return false; } - return $validity; + return true; } public function getHint(Element $element, string $value):string { - if($min = $element->getAttribute("min") ?: null) { - $min = (float)$min; + if(!is_numeric($value)) { + return "Field must be a number"; } - if($max = $element->getAttribute("max") ?: null) { - $max = (float)$max; + + $value = (float)$value; + + if($message = $this->getHintMinMax( + $value, + $element->getAttribute("min"), + $element->getAttribute("max"), + )) { + return $message; } - if($step = $element->getAttribute("step") ?: null) { - $step = (float)$step; + + if($message = $this->getHintStep( + $value, + $element->getAttribute("min"), + $element->getAttribute("step"), + )) { + return $message; } - $hint = ""; - if(is_numeric($value)) { - $value = (float)$value; + return ""; + } - if(!is_null($min) - && $value < $min) { - $hint = "Field value must not be less than $min"; + private function getHintMinMax( + float $value, + ?string $min, + ?string $max, + ):?string { + if(!is_null($min)) { + if($value < $min) { + return "Field value must not be lower than $min"; } - elseif(!is_null($max) - && $value > $max) { - $hint = "Field value must not be greater than $max"; + } + if(!is_null($max)) { + if($value > $max) { + return "Field value must not be higher than $max"; } - elseif(!is_null($step)) { - if(!is_null($min) - && ($value - $min) % $step !== 0) { - $hint = "Field value must be $min plus a multiple of $step"; - } - elseif($value % $step !== 0) { - $hint = "Field value must be a multiple of $step"; - } + } + + return null; + } + + private function getHintStep( + float $value, + ?string $min, + ?string $step, + ):?string { + if(!is_null($min)) { + $min = (float)$min; + + if(($value - $min) % $step !== 0) { + return "Field value must be $min plus a multiple of $step"; } } - else { - $hint = "Field must be a number"; + + if($step && $value % $step !== 0) { + return "Field value must be a multiple of $step"; + } + + return null; + } + + private function isValidMin(?string $min, float $value):bool { + if(is_null($min)) { + return true; + } + $min = (float)$min; + + return $value >= $min; + } + + private function isValidMax(?string $max, float $value):bool { + if(is_null($max)) { + return true; + } + $max = (float)$max; + + return $value <= $max; + } + + private function isValidStep(?string $min, ?string $step, float $value):bool { + if(!$step) { + return true; + } + $step = (float)$step; + + if(is_null($min)) { + return $value % $step === 0; } + $min = (float)$min; - return $hint; + return ($value - $min) % $step === 0; } } diff --git a/src/ValidationException.php b/src/ValidationException.php index 498d777..4009c0f 100644 --- a/src/ValidationException.php +++ b/src/ValidationException.php @@ -3,4 +3,4 @@ use RuntimeException; -class ValidationException extends RuntimeException {} \ No newline at end of file +class ValidationException extends RuntimeException {} diff --git a/src/Validator.php b/src/Validator.php index ff77097..e8c4b0c 100644 --- a/src/Validator.php +++ b/src/Validator.php @@ -3,6 +3,8 @@ use Gt\Dom\Element; use Gt\DomValidation\Rule\Rule; +use Stringable; +use Traversable; class Validator { protected ?ValidationRules $rules; @@ -16,35 +18,83 @@ public function __construct(?ValidationRules $rules = null) { $this->rules = $rules; } - /** @param array $inputKvp Associative array of user input */ - public function validate(Element $form, array $inputKvp):void { + /** @param iterable $inputKvp Associative array of user input */ + public function validate(Element $form, iterable|object $inputKvp):void { $this->errorList = new ErrorList(); + if(is_object($inputKvp)) { + $inputKvp = $this->convertObjectToKvp($inputKvp); + } + foreach($this->rules?->getAttributeRuleList() ?? [] as $attrString => $ruleArray) { - /** @var Element $element */ - foreach($form->querySelectorAll("[$attrString]") as $element) { - $name = $element->getAttribute("name"); - - foreach($ruleArray as $rule) { - if(!$rule->isValid($element, $inputKvp[$name] ?? "", $inputKvp)) { - $this->errorList->add( - $element, - $rule->getHint($element, $inputKvp[$name] ?? "") - ); - } - } - } + $this->buildErrorList( + $form, + $attrString, + $ruleArray, + $inputKvp + ); } $errorCount = count($this->errorList); if($errorCount > 0) { $collectiveNoun = $errorCount === 1 ? "is" : "are"; $fieldWord = $errorCount === 1 ? "field" : "fields"; - throw new ValidationException("There $collectiveNoun $errorCount invalid $fieldWord"); + throw new ValidationException( + "There $collectiveNoun $errorCount invalid $fieldWord" + ); } } public function getLastErrorList():ErrorList { return $this->errorList; } + + /** + * @param array $ruleArray + * @param array $inputKvp + */ + protected function buildErrorList( + Element $form, + int|string $attrString, + array $ruleArray, + array $inputKvp, + ): void { + /** @var Element $element */ + foreach ($form->querySelectorAll("[$attrString]") as $element) { + $name = $element->getAttribute("name"); + + foreach ($ruleArray as $rule) { + if (!$rule->isValid($element, $inputKvp[$name] ?? "", $inputKvp)) { + $this->errorList->add( + $element, + $rule->getHint($element, $inputKvp[$name] ?? "") + ); + } + } + } + } + + /** @return array */ + private function convertObjectToKvp(object $obj):array { + if(method_exists($obj, "asArray")) { + return $obj->asArray(); + } + + if($obj instanceof Traversable) { + return iterator_to_array($obj); + } + + $array = []; + foreach(get_object_vars($obj) as $key => $value) { + if(is_scalar($value) || $value instanceof Stringable) { + $value = (string)$value; + } + else { + $value = ""; + } + + $array[$key] = $value; + } + return $array; + } } diff --git a/test/phpunit/FormValidatorTest.php b/test/phpunit/FormValidatorTest.php deleted file mode 100644 index 0ea83b7..0000000 --- a/test/phpunit/FormValidatorTest.php +++ /dev/null @@ -1,78 +0,0 @@ -forms[0]; - $validator = new Validator(); - - $exception = null; - - try { - $validator->validate($form, [ - "username" => "g105b", - "password" => "hunter222222", - ]); - } - catch(ValidationException $exception) { - } - - self::assertNull($exception); - } - - /** - * In this example, the password field will be invalid if it contains - * the username. - */ - public function testCustomRule() { - $usernameNotWithinPasswordRule = new class extends Rule { - public function isValid(Element $element, string $value, array $inputKvp):bool { - if($element->type !== "password") { - return true; - } - - return !str_contains($value, $inputKvp["username"]); - } - - public function getHint(Element $element, string $value):string { - return "The password must not contain the username"; - } - }; - - $document = new HTMLDocument(Helper::HTML_USERNAME_PASSWORD); - $form = $document->forms[0]; - $rules = new DefaultValidationRules(); - $rules->addRule($usernameNotWithinPasswordRule); - $validator = new Validator($rules); - - $exception = null; - - try { - $validator->validate($form, [ - "username" => "g105b", - "password" => "g105b-password", - ]); - } - catch(ValidationException $exception) { - $errorArray = iterator_to_array($validator->getLastErrorList()); - self::assertCount(1, $errorArray); - $passwordErrorArray = $errorArray["password"]; - self::assertContains( - "The password must not contain the username", - $passwordErrorArray, - ); - } - - self::assertNotNull($exception); - } -} diff --git a/test/phpunit/Rule/EmailTypeTest.php b/test/phpunit/Rule/EmailTypeTest.php index 3d02f41..c010a1f 100644 --- a/test/phpunit/Rule/EmailTypeTest.php +++ b/test/phpunit/Rule/EmailTypeTest.php @@ -42,10 +42,10 @@ public function testEmailInvalid() { catch(ValidationException) { $errorArray = iterator_to_array($validator->getLastErrorList()); self::assertCount(1, $errorArray); - $emailErrorArray = $errorArray["email"]; - self::assertContains( + $emailError = $errorArray["email"]; + self::assertSame( "Field must be an email address", - $emailErrorArray + $emailError ); } } diff --git a/test/phpunit/Rule/MaxLengthTest.php b/test/phpunit/Rule/MaxLengthTest.php index f7d4087..ea59bf7 100644 --- a/test/phpunit/Rule/MaxLengthTest.php +++ b/test/phpunit/Rule/MaxLengthTest.php @@ -38,7 +38,7 @@ public function testMaxLength() { } catch(ValidationException $exception) { $errorList = iterator_to_array($validator->getLastErrorList()); - self::assertContains( + self::assertSame( "This field's value must not contain more than 120 characters", $errorList["tweet"] ); diff --git a/test/phpunit/Rule/MinLengthTest.php b/test/phpunit/Rule/MinLengthTest.php index 20bd01d..8d70c2a 100644 --- a/test/phpunit/Rule/MinLengthTest.php +++ b/test/phpunit/Rule/MinLengthTest.php @@ -40,7 +40,7 @@ public function testMinLength() { } catch(ValidationException $exception) { $errorList = iterator_to_array($validator->getLastErrorList()); - self::assertContains( + self::assertSame( "This field's value must contain at least 12 characters", $errorList["password"] ); diff --git a/test/phpunit/Rule/PatternTest.php b/test/phpunit/Rule/PatternTest.php index 55fb080..ada30f7 100644 --- a/test/phpunit/Rule/PatternTest.php +++ b/test/phpunit/Rule/PatternTest.php @@ -45,11 +45,10 @@ public function testPatternInvalid() { catch(ValidationException $exception) { $errorArray = iterator_to_array($validator->getLastErrorList()); self::assertCount(1, $errorArray); - $creditCardErrorArray = $errorArray["credit-card"]; - self::assertCount(1, $creditCardErrorArray); - self::assertEquals( + $creditCardError = $errorArray["credit-card"]; + self::assertSame( "This field does not match the required pattern", - $creditCardErrorArray[0] + $creditCardError ); } } @@ -68,15 +67,9 @@ public function testPatternWithMissingRequiredFields() { catch(ValidationException $exception) { $errorArray = iterator_to_array($validator->getLastErrorList()); self::assertCount(3, $errorArray); - $expiryMonthErrorArray = $errorArray["expiry-month"]; - $expiryYearErrorArray = $errorArray["expiry-year"]; - $amountErrorArray = $errorArray["amount"]; - self::assertCount(1, $expiryMonthErrorArray); - self::assertCount(1, $expiryYearErrorArray); - self::assertCount(1, $amountErrorArray); - self::assertEquals($expiryMonthErrorArray[0], "This field is required"); - self::assertEquals($expiryYearErrorArray[0], "This field is required"); - self::assertEquals($amountErrorArray[0], "This field is required"); + self::assertEquals($errorArray["expiry-month"], "This field is required"); + self::assertEquals($errorArray["expiry-year"], "This field is required"); + self::assertEquals($errorArray["amount"], "This field is required"); } } @@ -93,8 +86,8 @@ public function testPatternTitleShown() { } catch(ValidationException $exception) { $errorArray = iterator_to_array($validator->getLastErrorList()); - self::assertContains( - "This field does not match the required pattern: The 16 digit number on the front of your card", + self::assertSame( + "This field is required; This field does not match the required pattern: The 16 digit number on the front of your card", $errorArray["credit-card"] ); } diff --git a/test/phpunit/Rule/RequiredTest.php b/test/phpunit/Rule/RequiredTest.php index 77b7f15..dccd421 100644 --- a/test/phpunit/Rule/RequiredTest.php +++ b/test/phpunit/Rule/RequiredTest.php @@ -42,9 +42,8 @@ public function testSimpleMissingRequiredInputErrorList() { $validator->validate($form, ["username" => "g105b"]); } catch(ValidationException $exception) { - foreach($validator->getLastErrorList() as $name => $errors) { - self::assertIsArray($errors); - self::assertContains("This field is required", $errors); + foreach($validator->getLastErrorList() as $error) { + self::assertSame("This field is required; This field's value must contain at least 12 characters", $error); } } } @@ -61,10 +60,9 @@ public function testSimpleEmptyRequiredInputErrorList() { ]); } catch(ValidationException $exception) { - foreach($validator->getLastErrorList() as $name => $errors) { - self::assertIsArray($errors); - self::assertContains("This field is required", $errors); - self::assertEquals("password", $name); + foreach($validator->getLastErrorList() as $name => $error) { + self::assertSame("This field is required; This field's value must contain at least 12 characters", $error); + self::assertSame("password", $name); } } } diff --git a/test/phpunit/Rule/SelectElementTest.php b/test/phpunit/Rule/SelectElementTest.php index 9b8b16b..bd0738c 100644 --- a/test/phpunit/Rule/SelectElementTest.php +++ b/test/phpunit/Rule/SelectElementTest.php @@ -39,10 +39,9 @@ public function testSelectMissingRequired() { catch(ValidationException) { $errorArray = iterator_to_array($validator->getLastErrorList()); self::assertCount(1, $errorArray); - $currencyErrorArray = $errorArray["currency"]; - self::assertContains( + self::assertSame( "This field is required", - $currencyErrorArray + $errorArray["currency"] ); } } @@ -80,10 +79,9 @@ public function testSelectTextContentInvalid() { catch(ValidationException) { $errorArray = iterator_to_array($validator->getLastErrorList()); self::assertCount(1, $errorArray); - $currencyErrorArray = $errorArray["sort"]; - self::assertContains( + self::assertSame( "This field's value must match one of the available options", - $currencyErrorArray + $errorArray["sort"] ); } } @@ -122,10 +120,9 @@ public function testSelectValue_invalid() { catch(ValidationException $exception) { $errorArray = iterator_to_array($validator->getLastErrorList()); self::assertCount(1, $errorArray); - $currencyErrorArray = $errorArray["connections"]; - self::assertContains( + self::assertSame( "This field's value must match one of the available options", - $currencyErrorArray + $errorArray["connections"] ); } } @@ -144,20 +141,17 @@ public function testSelectTwoInvalidOptionsAndOneMissing() { catch(ValidationException $exception) { $errorArray = iterator_to_array($validator->getLastErrorList()); self::assertCount(3, $errorArray); - $currencyErrorArray = $errorArray["currency"]; - $sortErrorArray = $errorArray["sort"]; - $connectionsErrorArray = $errorArray["connections"]; - self::assertContains( + self::assertSame( "This field is required", - $currencyErrorArray + $errorArray["currency"] ); - self::assertContains( + self::assertSame( "This field's value must match one of the available options", - $sortErrorArray + $errorArray["sort"] ); - self::assertContains( + self::assertSame( "This field's value must match one of the available options", - $connectionsErrorArray + $errorArray["connections"] ); } } diff --git a/test/phpunit/Rule/TypeDateTest.php b/test/phpunit/Rule/TypeDateTest.php index 339fcb2..c212e47 100644 --- a/test/phpunit/Rule/TypeDateTest.php +++ b/test/phpunit/Rule/TypeDateTest.php @@ -39,10 +39,9 @@ public function testTypeDateInvalid() { catch(ValidationException $exception) { $errorArray = iterator_to_array($validator->getLastErrorList()); self::assertCount(1, $errorArray); - $dobErrorArray = $errorArray["dob"]; - self::assertContains( + self::assertSame( "Field must be a date in the format Y-m-d", - $dobErrorArray + $errorArray["dob"] ); } } @@ -78,10 +77,9 @@ public function testTypeMonthInvalid() { catch(ValidationException $exception) { $errorArray = iterator_to_array($validator->getLastErrorList()); self::assertCount(1, $errorArray); - $monthErrorArray = $errorArray["month"]; - self::assertContains( + self::assertSame( "Field must be a month in the format Y-m", - $monthErrorArray + $errorArray["month"] ); } } @@ -118,7 +116,7 @@ public function testTypeWeekInvalid() { $errorArray = iterator_to_array($validator->getLastErrorList()); self::assertCount(1, $errorArray); $monthErrorArray = $errorArray["week"]; - self::assertContains( + self::assertSame( "Field must be a week in the format Y-\WW", $monthErrorArray ); @@ -138,10 +136,9 @@ public function testTypeWeekOutOfBounds() { catch(ValidationException $exception) { $errorArray = iterator_to_array($validator->getLastErrorList()); self::assertCount(1, $errorArray); - $monthErrorArray = $errorArray["week"]; - self::assertContains( + self::assertSame( "Field must be a week in the format Y-\WW", - $monthErrorArray + $errorArray["week"] ); } } @@ -177,10 +174,9 @@ public function testTypeDatetimeLocalInvalid() { catch(ValidationException $exception) { $errorArray = iterator_to_array($validator->getLastErrorList()); self::assertCount(1, $errorArray); - $monthErrorArray = $errorArray["datetime"]; - self::assertContains( + self::assertSame( "Field must be a datetime-local in the format Y-m-d\TH:i", - $monthErrorArray + $errorArray["datetime"] ); } } @@ -216,11 +212,45 @@ public function testTypeTimeInvalid() { catch(ValidationException $exception) { $errorArray = iterator_to_array($validator->getLastErrorList()); self::assertCount(1, $errorArray); - $timeErrorArray = $errorArray["time"]; - self::assertContains( + self::assertSame( "Field must be a time in the format H:i", - $timeErrorArray + $errorArray["time"] ); } } + + public function testTypeAttributeMissing() { + $document = new HTMLDocument("
"); + $form = $document->forms[0]; + $validator = new Validator(); + + $exception = null; + try { + $validator->validate($form, [ + "time" => "3:37pm", + ]); + } + catch(ValidationException $exception) {} + self::assertNull($exception); + } + + public function testTypeNotKnown() { + $document = new HTMLDocument(Helper::HTML_DATE_TIME); + $form = $document->forms[0]; + $timeInput = $form->querySelector("[type='time']"); + $timeInput->type = "unknown"; + + $validator = new Validator(); + + $exception = null; + + try { + $validator->validate($form, [ + "time" => "3:37pm", + ]); + } + catch(ValidationException $exception) {} + + self::assertNull($exception); + } } diff --git a/test/phpunit/Rule/TypeNumberTest.php b/test/phpunit/Rule/TypeNumberTest.php index 255d119..a1ada71 100644 --- a/test/phpunit/Rule/TypeNumberTest.php +++ b/test/phpunit/Rule/TypeNumberTest.php @@ -1,7 +1,9 @@ "100", ]); } - catch(ValidationException $exception) { + catch(ValidationException) { $errorArray = iterator_to_array($validator->getLastErrorList()); self::assertCount(1, $errorArray); - $expiryMonthError = $errorArray["expiry-month"]; - self::assertContains( + self::assertSame( "Field must be a number", - $expiryMonthError + $errorArray["expiry-month"] ); } } @@ -131,10 +132,9 @@ public function testStepInvalid() { catch(ValidationException $exception) { $errorArray = iterator_to_array($validator->getLastErrorList()); self::assertCount(1, $errorArray); - $step1Error = $errorArray["step1"]; - self::assertContains( + self::assertSame( "Field value must be a multiple of 7", - $step1Error + $errorArray["step1"] ); } } @@ -188,10 +188,9 @@ public function testStepStartingFrom2Invalid() { catch(ValidationException $exception) { $errorArray = iterator_to_array($validator->getLastErrorList()); self::assertCount(1, $errorArray); - $step1Error = $errorArray["step2"]; - self::assertContains( + self::assertSame( "Field value must be 2 plus a multiple of 7", - $step1Error + $errorArray["step2"] ); } } @@ -203,16 +202,15 @@ public function testStepWithMinBust() { try { $validator->validate($form, [ - "step2" => -4, + "step2" => "-4", ]); } - catch(ValidationException $exception) { + catch(ValidationException) { $errorArray = iterator_to_array($validator->getLastErrorList()); self::assertCount(1, $errorArray); - $step1Error = $errorArray["step2"]; - self::assertContains( - "Field value must not be less than 2", - $step1Error + self::assertSame( + "Field value must not be lower than 2", + $errorArray["step2"] ); } } @@ -229,8 +227,7 @@ public function testStepWithMax() { "step3" => 7.2 * 3, // within range ]); } - catch(ValidationException $exception) { - } + catch(ValidationException $exception) {} self::assertNull($exception); } @@ -248,10 +245,9 @@ public function testStepWithMaxBust() { catch(ValidationException $exception) { $errorArray = iterator_to_array($validator->getLastErrorList()); self::assertCount(1, $errorArray); - $step1Error = $errorArray["step3"]; - self::assertContains( - "Field value must not be greater than 25.1", - $step1Error + self::assertSame( + "Field value must not be higher than 25.1", + $errorArray["step3"] ); } } @@ -284,14 +280,21 @@ public function testStepWithDecimalStartBust() { "step4" => 3.5 + (7.2 * 4), ]); } - catch(ValidationException $exception) { + catch(ValidationException) { $errorArray = iterator_to_array($validator->getLastErrorList()); - self::assertCount(1, $errorArray); - $step1Error = $errorArray["step4"]; - self::assertContains( - "Field value must not be greater than 25.1", - $step1Error + self::assertSame( + "Field value must not be higher than 25.1", + $errorArray["step4"] ); } } + + public function testGetHint_ok() { + $element = self::createMock(Element::class); + $element->method("getAttribute") + ->willReturn(null); + + $sut = new TypeNumber(); + self::assertEmpty($sut->getHint($element, 1)); + } } diff --git a/test/phpunit/Rule/RadioElementTest.php b/test/phpunit/Rule/TypeRadioTest.php similarity index 78% rename from test/phpunit/Rule/RadioElementTest.php rename to test/phpunit/Rule/TypeRadioTest.php index b0afd60..d962e21 100644 --- a/test/phpunit/Rule/RadioElementTest.php +++ b/test/phpunit/Rule/TypeRadioTest.php @@ -1,13 +1,16 @@ forms[0]; @@ -38,11 +41,9 @@ public function testRadioMissingRequired() { } catch(ValidationException) { $errorArray = iterator_to_array($validator->getLastErrorList()); - self::assertCount(1, $errorArray); - $currencyErrorArray = $errorArray["currency"]; - self::assertContains( + self::assertSame( "This field is required", - $currencyErrorArray + $errorArray["currency"], ); } } @@ -80,11 +81,20 @@ public function testRadioTextContentInvalid() { catch(ValidationException) { $errorArray = iterator_to_array($validator->getLastErrorList()); self::assertCount(1, $errorArray); - $currencyErrorArray = $errorArray["sort"]; - self::assertContains( + self::assertSame( "This field's value must match one of the available options", - $currencyErrorArray + $errorArray["sort"] ); } } + + public function testIsValid_noForm() { + $document = new HTMLDocument(); + $element = $document->createElement("input"); + $element->type = "radio"; + $sut = new TypeRadio(); + + $validity = $sut->isValid($element, "anything", []); + self::assertTrue($validity); + } } diff --git a/test/phpunit/Rule/UrlTypeTest.php b/test/phpunit/Rule/UrlTypeTest.php index aa4bd34..53ec791 100644 --- a/test/phpunit/Rule/UrlTypeTest.php +++ b/test/phpunit/Rule/UrlTypeTest.php @@ -41,10 +41,9 @@ public function testUrlInvalid() { catch(ValidationException) { $errorArray = iterator_to_array($validator->getLastErrorList()); self::assertCount(1, $errorArray); - $emailErrorArray = $errorArray["website"]; - self::assertContains( + self::assertSame( "Field must be a URL", - $emailErrorArray + $errorArray["website"] ); } } diff --git a/test/phpunit/ValidatorTest.php b/test/phpunit/ValidatorTest.php new file mode 100644 index 0000000..fbd44bb --- /dev/null +++ b/test/phpunit/ValidatorTest.php @@ -0,0 +1,157 @@ +forms[0]; + $validator = new Validator(); + + $exception = null; + + try { + $validator->validate($form, [ + "username" => "g105b", + "password" => "hunter222222", + ]); + } + catch(ValidationException $exception) { + } + + self::assertNull($exception); + } + + /** + * In this example, the password field will be invalid if it contains + * the username. + */ + public function testCustomRule() { + $usernameNotWithinPasswordRule = new class extends Rule { + public function isValid(Element $element, string $value, array $inputKvp):bool { + if($element->type !== "password") { + return true; + } + + return !str_contains($value, $inputKvp["username"]); + } + + public function getHint(Element $element, string $value):string { + return "The password must not contain the username"; + } + }; + + $document = new HTMLDocument(Helper::HTML_USERNAME_PASSWORD); + $form = $document->forms[0]; + $rules = new DefaultValidationRules(); + $rules->addRule($usernameNotWithinPasswordRule); + $validator = new Validator($rules); + + $exception = null; + + try { + $validator->validate($form, [ + "username" => "g105b", + "password" => "g105b-password", + ]); + } + catch(ValidationException $exception) { + $errorArray = iterator_to_array($validator->getLastErrorList()); + self::assertCount(1, $errorArray); + self::assertSame( + "The password must not contain the username", + $errorArray["password"], + ); + } + + self::assertNotNull($exception); + } + + public function testValidate_objectAsArray():void { + $kvpInput = [ + "name" => "Andrew Chi-Chih Yao", + "dob" => "1946-12-24", + ]; + + /** @var ArrayIterator|MockObject $inputObject */ + $inputObject = self::getMockBuilder(ArrayIterator::class) + ->addMethods(["asArray"]) + ->getMock(); + $inputObject->expects(self::once()) + ->method("asArray") + ->willReturn($kvpInput); + + $form = self::createMock(Element::class); + + $sut = new Validator(); + $sut->validate($form, $inputObject); + } + + public function testValidate_kvpObject():void { + $document = new HTMLDocument(Helper::HTML_USERNAME_PASSWORD); + $form = $document->forms[0]; + $validator = new Validator(); + $input = new stdClass(); + $input->username = "g105b"; + $input->password = "hunter222222"; + + $exception = null; + + try { + $validator->validate($form, $input); + } + catch(ValidationException $exception) {} + + self::assertNull($exception); + } + + public function testValidate_kvpTraversible():void { + $document = new HTMLDocument(Helper::HTML_USERNAME_PASSWORD); + $form = $document->forms[0]; + $validator = new Validator(); + $input = new class([ + "username" => "g105b", + "password" => "hunter222222", + ]) extends ArrayIterator {}; + + $exception = null; + + try { + $validator->validate($form, $input); + } + catch(ValidationException $exception) {} + + self::assertNull($exception); + } + + public function testValidate_kvpObject_notStringable():void { + $document = new HTMLDocument(Helper::HTML_USERNAME_PASSWORD); + $form = $document->forms[0]; + $validator = new Validator(); + $input = new stdClass(); + $input->username = "g105b"; + $input->password = "hunter222222"; + $input->somethingElse = new stdClass(); + + $exception = null; + + try { + $validator->validate($form, $input); + } + catch(ValidationException $exception) {} + + self::assertNull($exception); + } +}