From 353fa7a1a373031dc19c2661d839e180ab36720b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Dobe=C5=A1?= Date: Tue, 3 Jun 2025 13:14:51 +0200 Subject: [PATCH] Initial version --- .editorconfig | 14 + .gitattributes | 9 + .github/workflows/checks.yml | 75 + .gitignore | 5 + LICENSE | 28 + composer.json | 53 + composer.lock | 2559 +++++++++++++++++ extension.neon | 22 + phpstan-baseline.neon | 13 + phpstan.dist.neon | 36 + phpunit.xml.dist | 17 + readme.md | 44 + src/GraphQL/Adapter.php | 20 + src/GraphQL/Config.php | 39 + src/GraphQL/CorrespondanceRule.php | 370 +++ src/GraphQL/Helpers.php | 43 + src/GraphQL/SchemaClassGenerator.php | 246 ++ src/GraphQL/SchemaServiceOraculum.php | 126 + src/GraphQL/bootstrap.php | 20 + .../AbstractCorrespondanceRuleTest.php | 61 + tests-shared/DummyCollector.php | 27 + .../InvalidParentTypeNameFieldResolver.php | 19 + tests-shared/Resolvers/ObjectType.php | 20 + tests-shared/Resolvers/Person.php | 13 + .../Resolvers/QueryArrayTypeFieldResolver.php | 21 + ...yInvalidArgumentsMismatchFieldResolver.php | 19 + ...validStringResolvedAsBoolFieldResolver.php | 19 + .../QueryObjectTypeFieldResolver.php | 19 + ...oviderOfInvalidParentTypeFieldResolver.php | 19 + .../QueryValidDeferredFieldResolver.php | 19 + .../QueryValidNonNullStringFieldResolver.php | 19 + tests-shared/schema.graphqls | 26 + tests/.gitignore | 1 + tests/CorrespondanceRuleTest.extension.neon | 10 + tests/CorrespondanceRuleTest.php | 16 + tests/CustomAdapter.php | 50 + 36 files changed, 4117 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/workflows/checks.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 extension.neon create mode 100644 phpstan-baseline.neon create mode 100644 phpstan.dist.neon create mode 100644 phpunit.xml.dist create mode 100644 readme.md create mode 100644 src/GraphQL/Adapter.php create mode 100644 src/GraphQL/Config.php create mode 100644 src/GraphQL/CorrespondanceRule.php create mode 100644 src/GraphQL/Helpers.php create mode 100644 src/GraphQL/SchemaClassGenerator.php create mode 100644 src/GraphQL/SchemaServiceOraculum.php create mode 100644 src/GraphQL/bootstrap.php create mode 100644 tests-shared/AbstractCorrespondanceRuleTest.php create mode 100644 tests-shared/DummyCollector.php create mode 100644 tests-shared/Resolvers/InvalidParentTypeNameFieldResolver.php create mode 100644 tests-shared/Resolvers/ObjectType.php create mode 100644 tests-shared/Resolvers/Person.php create mode 100644 tests-shared/Resolvers/QueryArrayTypeFieldResolver.php create mode 100644 tests-shared/Resolvers/QueryInvalidArgumentsMismatchFieldResolver.php create mode 100644 tests-shared/Resolvers/QueryInvalidStringResolvedAsBoolFieldResolver.php create mode 100644 tests-shared/Resolvers/QueryObjectTypeFieldResolver.php create mode 100644 tests-shared/Resolvers/QueryProviderOfInvalidParentTypeFieldResolver.php create mode 100644 tests-shared/Resolvers/QueryValidDeferredFieldResolver.php create mode 100644 tests-shared/Resolvers/QueryValidNonNullStringFieldResolver.php create mode 100644 tests-shared/schema.graphqls create mode 100644 tests/.gitignore create mode 100644 tests/CorrespondanceRuleTest.extension.neon create mode 100644 tests/CorrespondanceRuleTest.php create mode 100644 tests/CustomAdapter.php diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bc541e8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +end_of_line = lf +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{md,neon,yml}] +indent_size = 2 +indent_style = space + +[phpstan-baseline.neon] +indent_style = tab diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..acbcff9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.github export-ignore +phpstan-baseline.neon export-ignore +phpstan.neon export-ignore +tests/ export-ignore + +*.php* diff=php linguist-language=PHP diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..5341bc9 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,75 @@ +name: checks + +on: + - push + +jobs: + static_analysis: + name: Static analysis + + runs-on: ubuntu-latest + + env: + COMPOSER_NO_INTERACTION: "1" + + steps: + - uses: actions/checkout@v4 + + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + coverage: none + + - run: composer install --ansi --no-progress --prefer-dist + + - name: Run parallel-lint + run: composer run lint + + - name: Run PHPStan + run: composer run phpstan + + tests: + name: PHP ${{ matrix.php }} tests on ${{ matrix.os }} with ${{ matrix.deps }} deps + + needs: + - static_analysis + + strategy: + matrix: + deps: + - stable + - lowest + os: + - macos-latest + - ubuntu-latest + - windows-latest + php: + - '8.4' + + fail-fast: false + + runs-on: ${{ matrix.os }} + + env: + COMPOSER_NO_INTERACTION: "1" + + steps: + - uses: actions/checkout@v4 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - run: composer install --ansi --no-progress --prefer-dist + + - run: composer update --ansi --no-progress --prefer-lowest + if: matrix.deps == 'lowest' + + - name: Run tests + run: composer run test + + - if: failure() + uses: actions/upload-artifact@v4 + with: + path: tests/output diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..13e687e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +.phpunit.cache +.phpunit.result.cache +tests-temp +vendor diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8c8493d --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2025, Vojtěch Dobeš + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..304fbc2 --- /dev/null +++ b/composer.json @@ -0,0 +1,53 @@ +{ + "authors": [ + { + "name": "Vojtěch Dobeš", + "homepage": "https://vojtechdobes.com" + } + ], + "autoload": { + "psr-4": { + "Vojtechdobes\\PHPStan\\": "src/", + "Vojtechdobes\\TestsShared\\": "tests-shared/" + } + }, + "autoload-dev": { + "psr-4": { + "Vojtechdobes\\Tests\\": "tests/" + } + }, + "config": { + "sort-packages": true + }, + "keywords": [ + "ci", + "graphql", + "phpstan", + "phpstan-rules", + "static-analysis", + "static-code-analysis" + ], + "license": [ + "BSD-3-Clause" + ], + "name": "vojtech-dobes/phpstan-php-graphql-server", + "require": { + "php": "~8.4" + }, + "require-dev": { + "nette/di": "^3.2", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpstan/phpstan": "^2.1.12", + "phpstan/phpstan-strict-rules": "^2.0.4", + "phpunit/phpunit": "^12.1", + "spaze/phpstan-disallowed-calls": "^4.5.0", + "tracy/tracy": "^2.10.9", + "vojtech-dobes/php-grammar-processing": "dev-master@dev", + "vojtech-dobes/php-graphql-server": "dev-master@dev" + }, + "scripts": { + "lint": "parallel-lint src tests", + "phpstan": "phpstan analyse --memory-limit 256M", + "test": "composer dump-autoload && phpunit tests" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..1cd3a5c --- /dev/null +++ b/composer.lock @@ -0,0 +1,2559 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "ae327a4f793abad51c45e066374c7f89", + "packages": [], + "packages-dev": [ + { + "name": "guzzlehttp/promises", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.2.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-03-27T13:27:01+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-04-29T12:36:36+00:00" + }, + { + "name": "nette/di", + "version": "v3.2.4", + "source": { + "type": "git", + "url": "https://github.com/nette/di.git", + "reference": "57f923a7af32435b6e4921c0adbc70c619625a17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/di/zipball/57f923a7af32435b6e4921c0adbc70c619625a17", + "reference": "57f923a7af32435b6e4921c0adbc70c619625a17", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-tokenizer": "*", + "nette/neon": "^3.3 || ^4.0", + "nette/php-generator": "^4.1.6", + "nette/robot-loader": "^4.0", + "nette/schema": "^1.2.5", + "nette/utils": "^4.0", + "php": "8.1 - 8.4" + }, + "require-dev": { + "nette/tester": "^2.5.2", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "💎 Nette Dependency Injection Container: Flexible, compiled and full-featured DIC with perfectly usable autowiring and support for all new PHP features.", + "homepage": "https://nette.org", + "keywords": [ + "compiled", + "di", + "dic", + "factory", + "ioc", + "nette", + "static" + ], + "support": { + "issues": "https://github.com/nette/di/issues", + "source": "https://github.com/nette/di/tree/v3.2.4" + }, + "time": "2025-01-10T04:57:37+00:00" + }, + { + "name": "nette/neon", + "version": "v3.4.4", + "source": { + "type": "git", + "url": "https://github.com/nette/neon.git", + "reference": "3411aa86b104e2d5b7e760da4600865ead963c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/neon/zipball/3411aa86b104e2d5b7e760da4600865ead963c3c", + "reference": "3411aa86b104e2d5b7e760da4600865ead963c3c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "8.0 - 8.4" + }, + "require-dev": { + "nette/tester": "^2.4", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.7" + }, + "bin": [ + "bin/neon-lint" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🍸 Nette NEON: encodes and decodes NEON file format.", + "homepage": "https://ne-on.org", + "keywords": [ + "export", + "import", + "neon", + "nette", + "yaml" + ], + "support": { + "issues": "https://github.com/nette/neon/issues", + "source": "https://github.com/nette/neon/tree/v3.4.4" + }, + "time": "2024-10-04T22:00:08+00:00" + }, + { + "name": "nette/php-generator", + "version": "v4.1.8", + "source": { + "type": "git", + "url": "https://github.com/nette/php-generator.git", + "reference": "42806049a7774a2bd316c958f5dcf01c6b5c56fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/php-generator/zipball/42806049a7774a2bd316c958f5dcf01c6b5c56fa", + "reference": "42806049a7774a2bd316c958f5dcf01c6b5c56fa", + "shasum": "" + }, + "require": { + "nette/utils": "^3.2.9 || ^4.0", + "php": "8.0 - 8.4" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "^2.4", + "nikic/php-parser": "^4.18 || ^5.0", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.8" + }, + "suggest": { + "nikic/php-parser": "to use ClassType::from(withBodies: true) & ClassType::fromCode()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 8.4 features.", + "homepage": "https://nette.org", + "keywords": [ + "code", + "nette", + "php", + "scaffolding" + ], + "support": { + "issues": "https://github.com/nette/php-generator/issues", + "source": "https://github.com/nette/php-generator/tree/v4.1.8" + }, + "time": "2025-03-31T00:29:29+00:00" + }, + { + "name": "nette/robot-loader", + "version": "v4.0.3", + "source": { + "type": "git", + "url": "https://github.com/nette/robot-loader.git", + "reference": "45d67753fb4865bb718e9a6c9be69cc9470137b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/robot-loader/zipball/45d67753fb4865bb718e9a6c9be69cc9470137b7", + "reference": "45d67753fb4865bb718e9a6c9be69cc9470137b7", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "nette/utils": "^4.0", + "php": "8.0 - 8.4" + }, + "require-dev": { + "nette/tester": "^2.4", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🍀 Nette RobotLoader: high performance and comfortable autoloader that will search and autoload classes within your application.", + "homepage": "https://nette.org", + "keywords": [ + "autoload", + "class", + "interface", + "nette", + "trait" + ], + "support": { + "issues": "https://github.com/nette/robot-loader/issues", + "source": "https://github.com/nette/robot-loader/tree/v4.0.3" + }, + "time": "2024-06-18T20:26:39+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.2", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "da801d52f0354f70a638673c4a0f04e16529431d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", + "reference": "da801d52f0354f70a638673c4a0f04e16529431d", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.4" + }, + "require-dev": { + "nette/tester": "^2.5.2", + "phpstan/phpstan-nette": "^1.0", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.2" + }, + "time": "2024-10-06T23:10:23+00:00" + }, + { + "name": "nette/utils", + "version": "v4.0.6", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "ce708655043c7050eb050df361c5e313cf708309" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/ce708655043c7050eb050df361c5e313cf708309", + "reference": "ce708655043c7050eb050df361c5e313cf708309", + "shasum": "" + }, + "require": { + "php": "8.0 - 8.4" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "^2.5", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.0.6" + }, + "time": "2025-03-30T21:06:30+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.5.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" + }, + "time": "2025-05-31T08:24:38+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "php-parallel-lint/php-parallel-lint", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/php-parallel-lint/PHP-Parallel-Lint.git", + "reference": "6db563514f27e19595a19f45a4bf757b6401194e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-parallel-lint/PHP-Parallel-Lint/zipball/6db563514f27e19595a19f45a4bf757b6401194e", + "reference": "6db563514f27e19595a19f45a4bf757b6401194e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=5.3.0" + }, + "replace": { + "grogy/php-parallel-lint": "*", + "jakub-onderka/php-parallel-lint": "*" + }, + "require-dev": { + "nette/tester": "^1.3 || ^2.0", + "php-parallel-lint/php-console-highlighter": "0.* || ^1.0", + "squizlabs/php_codesniffer": "^3.6" + }, + "suggest": { + "php-parallel-lint/php-console-highlighter": "Highlight syntax in code snippet" + }, + "bin": [ + "parallel-lint" + ], + "type": "library", + "autoload": { + "classmap": [ + "./src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Jakub Onderka", + "email": "ahoj@jakubonderka.cz" + } + ], + "description": "This tool checks the syntax of PHP files about 20x faster than serial check.", + "homepage": "https://github.com/php-parallel-lint/PHP-Parallel-Lint", + "keywords": [ + "lint", + "static analysis" + ], + "support": { + "issues": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/issues", + "source": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/tree/v1.4.0" + }, + "time": "2024-03-27T12:14:49+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.1.17", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053", + "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-05-21T20:55:28+00:00" + }, + { + "name": "phpstan/phpstan-strict-rules", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-strict-rules.git", + "reference": "3e139cbe67fafa3588e1dbe27ca50f31fdb6236a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/3e139cbe67fafa3588e1dbe27ca50f31fdb6236a", + "reference": "3e139cbe67fafa3588e1dbe27ca50f31fdb6236a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0.4" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Extra strict and opinionated rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.4" + }, + "time": "2025-03-18T11:42:40+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.3.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "9075a8efc66e11bc55c319062e147bdb06777267" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/9075a8efc66e11bc55c319062e147bdb06777267", + "reference": "9075a8efc66e11bc55c319062e147bdb06777267", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.4.0", + "php": ">=8.3", + "phpunit/php-file-iterator": "^6.0", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.3.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-05-23T15:49:03+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "961bc913d42fe24a257bfff826a5068079ac7782" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/961bc913d42fe24a257bfff826a5068079ac7782", + "reference": "961bc913d42fe24a257bfff826a5068079ac7782", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:37+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:16+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:38+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "12.1.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "2fdf0056c673c8f0f1eed00030be5f8243c1e6e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2fdf0056c673c8f0f1eed00030be5f8243c1e6e0", + "reference": "2fdf0056c673c8f0f1eed00030be5f8243c1e6e0", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.1", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.2.1", + "phpunit/php-file-iterator": "^6.0.0", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.0.0", + "sebastian/comparator": "^7.0.1", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.0.1", + "sebastian/exporter": "^7.0.0", + "sebastian/global-state": "^8.0.0", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/type": "^6.0.2", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.1-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.1.6" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-05-21T12:36:31+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "6d584c727d9114bcdc14c86711cd1cad51778e7c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/6d584c727d9114bcdc14c86711cd1cad51778e7c", + "reference": "6d584c727d9114bcdc14c86711cd1cad51778e7c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:53:50+00:00" + }, + { + "name": "sebastian/comparator", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "b478f34614f934e0291598d0c08cbaba9644bee5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/b478f34614f934e0291598d0c08cbaba9644bee5", + "reference": "b478f34614f934e0291598d0c08cbaba9644bee5", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-07T07:00:32+00:00" + }, + { + "name": "sebastian/complexity", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "8.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "d364b9e5d0d3b18a2573351a1786fbf96b7e0792" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d364b9e5d0d3b18a2573351a1786fbf96b7e0792", + "reference": "d364b9e5d0d3b18a2573351a1786fbf96b7e0792", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T15:05:44+00:00" + }, + { + "name": "sebastian/exporter", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "76432aafc58d50691a00d86d0632f1217a47b688" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/76432aafc58d50691a00d86d0632f1217a47b688", + "reference": "76432aafc58d50691a00d86d0632f1217a47b688", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:56:42+00:00" + }, + { + "name": "sebastian/global-state", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "570a2aeb26d40f057af686d63c4e99b075fb6cbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/570a2aeb26d40f057af686d63c4e99b075fb6cbc", + "reference": "570a2aeb26d40f057af686d63c4e99b075fb6cbc", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:56:59+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:28+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:48+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:17+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "c405ae3a63e01b32eb71577f8ec1604e39858a7c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/c405ae3a63e01b32eb71577f8ec1604e39858a7c", + "reference": "c405ae3a63e01b32eb71577f8ec1604e39858a7c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:01+00:00" + }, + { + "name": "sebastian/type", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "1d7cd6e514384c36d7a390347f57c385d4be6069" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/1d7cd6e514384c36d7a390347f57c385d4be6069", + "reference": "1d7cd6e514384c36d7a390347f57c385d4be6069", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-18T13:37:31+00:00" + }, + { + "name": "sebastian/version", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:38+00:00" + }, + { + "name": "spaze/phpstan-disallowed-calls", + "version": "v4.5.0", + "source": { + "type": "git", + "url": "https://github.com/spaze/phpstan-disallowed-calls.git", + "reference": "1c5e6996bd75a1460f5e2683fc4294665b37bee2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spaze/phpstan-disallowed-calls/zipball/1c5e6996bd75a1460f5e2683fc4294665b37bee2", + "reference": "1c5e6996bd75a1460f5e2683fc4294665b37bee2", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^1.12.6 || ^2.0" + }, + "require-dev": { + "nette/neon": "^3.3.1", + "nikic/php-parser": "^4.13.2 || ^5.0", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^1.2 || ^2.0", + "phpunit/phpunit": "^8.5.14 || ^10.1 || ^11.0 || ^12.0", + "spaze/coding-standard": "^1.8" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "Spaze\\PHPStan\\Rules\\Disallowed\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michal Špaček", + "email": "mail@michalspacek.cz", + "homepage": "https://www.michalspacek.cz" + } + ], + "description": "PHPStan rules to detect disallowed method & function calls, constant, namespace, attribute & superglobal usages, with powerful rules to re-allow a call or a usage in places where it should be allowed.", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/spaze/phpstan-disallowed-calls/issues", + "source": "https://github.com/spaze/phpstan-disallowed-calls/tree/v4.5.0" + }, + "funding": [ + { + "url": "https://github.com/spaze", + "type": "github" + } + ], + "time": "2025-04-10T19:01:43+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + }, + { + "name": "tracy/tracy", + "version": "v2.10.10", + "source": { + "type": "git", + "url": "https://github.com/nette/tracy.git", + "reference": "32303e02c222eea8571402a8310fc3fe70422c37" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/tracy/zipball/32303e02c222eea8571402a8310fc3fe70422c37", + "reference": "32303e02c222eea8571402a8310fc3fe70422c37", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-session": "*", + "php": "8.0 - 8.4" + }, + "conflict": { + "nette/di": "<3.0" + }, + "require-dev": { + "latte/latte": "^2.5 || ^3.0", + "nette/di": "^3.0", + "nette/http": "^3.0", + "nette/mail": "^3.0 || ^4.0", + "nette/tester": "^2.2", + "nette/utils": "^3.0 || ^4.0", + "phpstan/phpstan": "^1.0", + "psr/log": "^1.0 || ^2.0 || ^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.10-dev" + } + }, + "autoload": { + "files": [ + "src/Tracy/functions.php" + ], + "classmap": [ + "src" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "😎 Tracy: the addictive tool to ease debugging PHP code for cool developers. Friendly design, logging, profiler, advanced features like debugging AJAX calls or CLI support. You will love it.", + "homepage": "https://tracy.nette.org", + "keywords": [ + "Xdebug", + "debug", + "debugger", + "nette", + "profiler" + ], + "support": { + "issues": "https://github.com/nette/tracy/issues", + "source": "https://github.com/nette/tracy/tree/v2.10.10" + }, + "time": "2025-04-28T14:35:15+00:00" + }, + { + "name": "vojtech-dobes/php-grammar-processing", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/vojtech-dobes/php-grammar-processing.git", + "reference": "f90e1f0331464a781e6ee0271ec78cf5cd32d5f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vojtech-dobes/php-grammar-processing/zipball/f90e1f0331464a781e6ee0271ec78cf5cd32d5f0", + "reference": "f90e1f0331464a781e6ee0271ec78cf5cd32d5f0", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "php": "~8.4" + }, + "require-dev": { + "nette/tester": "^2.5.4", + "phpstan/phpstan": "^2.1.12", + "phpstan/phpstan-strict-rules": "^2.0.4", + "spaze/phpstan-disallowed-calls": "^4.5.0", + "tracy/tracy": "^2.10.9" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "Vojtechdobes\\GrammarProcessing\\": "src/GrammarProcessing" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Vojtěch Dobeš", + "homepage": "https://vojtechdobes.com" + } + ], + "description": "Library for tokenization, abstract syntax tree parsing & interpretation", + "keywords": [ + "ast", + "context-free", + "grammar", + "language", + "lexer", + "parser", + "syntax", + "token" + ], + "support": { + "issues": "https://github.com/vojtech-dobes/php-grammar-processing/issues", + "source": "https://github.com/vojtech-dobes/php-grammar-processing/tree/master" + }, + "time": "2025-04-29T15:17:38+00:00" + }, + { + "name": "vojtech-dobes/php-graphql-server", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/vojtech-dobes/php-graphql-server.git", + "reference": "6002078efa50c9c74e0aeb25069944db2a74146e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vojtech-dobes/php-graphql-server/zipball/6002078efa50c9c74e0aeb25069944db2a74146e", + "reference": "6002078efa50c9c74e0aeb25069944db2a74146e", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "guzzlehttp/promises": "^2.0.3", + "nette/php-generator": "^4.0", + "php": "~8.4", + "vojtech-dobes/php-grammar-processing": "dev-master" + }, + "require-dev": { + "jiripudil/phpstan-sealed-classes": "^1.3.0", + "nette/tester": "^2.5.4", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpstan/phpstan": "^2.1.12", + "phpstan/phpstan-strict-rules": "^2.0.4", + "psr/log": "^3.0", + "spaze/phpstan-disallowed-calls": "^4.5.0", + "tracy/tracy": "^2.10.9" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "Vojtechdobes\\GraphQL\\": "src/GraphQL" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Vojtěch Dobeš", + "homepage": "https://vojtechdobes.com" + } + ], + "keywords": [ + "api", + "graphql" + ], + "support": { + "issues": "https://github.com/vojtech-dobes/php-graphql-server/issues", + "source": "https://github.com/vojtech-dobes/php-graphql-server/tree/master" + }, + "time": "2025-06-02T14:16:36+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "vojtech-dobes/php-grammar-processing": 20, + "vojtech-dobes/php-graphql-server": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "~8.4" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/extension.neon b/extension.neon new file mode 100644 index 0000000..d7a6f94 --- /dev/null +++ b/extension.neon @@ -0,0 +1,22 @@ +parameters: + bootstrapFiles: + - src/GraphQL/bootstrap.php + + scanDirectories: + - %graphql.generatedDir% + +parametersSchema: + graphql: structure([ + generatedDir: string() + schemas: listOf(schema(string(), assert('is_file', 'Schema path must exist'))) + ]) + +services: + - class: Vojtechdobes\PHPStan\GraphQL\Config + arguments: + generatedDir: %graphql.generatedDir% + schemas: %graphql.schemas% + + - class: Vojtechdobes\PHPStan\GraphQL\CorrespondanceRule + tags: + - phpstan.rules.rule diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..841e0c9 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,13 @@ +parameters: + ignoreErrors: + - + message: '#^Method Vojtechdobes\\PHPStan\\GraphQL\\Helpers\:\:normalizeSchema\(\) throws checked exception Exception but it''s missing from the PHPDoc @throws tag\.$#' + identifier: missingType.checkedException + count: 1 + path: src/GraphQL/Helpers.php + + - + message: '#^Method Vojtechdobes\\PHPStan\\GraphQL\\SchemaServiceOraculum\:\:getFieldResolverType\(\) should return PHPStan\\Type\\ObjectType but returns PHPStan\\Type\\Type\.$#' + identifier: return.type + count: 1 + path: src/GraphQL/SchemaServiceOraculum.php diff --git a/phpstan.dist.neon b/phpstan.dist.neon new file mode 100644 index 0000000..f514c55 --- /dev/null +++ b/phpstan.dist.neon @@ -0,0 +1,36 @@ +includes: + - phpstan-baseline.neon + - vendor/phpstan/phpstan-strict-rules/rules.neon + - vendor/spaze/phpstan-disallowed-calls/extension.neon + +parameters: + checkMissingCallableSignature: true + disallowedFunctionCalls: + - function: + - 'dump()' + - 'var_dump()' + message: 'avoid committing debug calls' + + exceptions: + check: + missingCheckedExceptionInThrows: true + tooWideThrowType: true + + uncheckedExceptionClasses: + - Error + - LogicException + - Nette\IOException + - PHPStan\ShouldNotHappenException + + excludePaths: + - tests/generated-graphql (?) + + level: 8 + + paths: + - src + - tests + - tests-shared + + strictRules: + disallowedShortTernary: false diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..5f2cf32 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,17 @@ + + + + + + diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..c869c2c --- /dev/null +++ b/readme.md @@ -0,0 +1,44 @@ +# PHPStan extension for [PHP GraphQL Server](https://github.com/vojtech-dobes/php-graphql-server) + +![Checks](https://github.com/vojtech-dobes/phpstan-php-graphql-server/actions/workflows/checks.yml/badge.svg?branch=master&event=push) + +This is super-convenient companion if you use [`vojtech-dobes/php-graphql-server`](https://github.com/vojtech-dobes/php-graphql-server) and [PHPStan](https://phpstan.org/). With this extension, PHPStan will be able to point out: + +- mismatch between GraphQL Schema & what your resolvers actually return +- mismatch between GraphQL Schema & what your resolvers actually accept as arguments +- mismatch between declared parent value type and what resolver will actually receive +- supports utility resolvers like `PropertyFieldResolver` etc. + + + +## Installation + +To install the latest version, run the following command: + +``` +composer require vojtech-dobes/phpstan-php-graphql-server +``` + +Then you can register in your `phpstan.neon`: + +```neon +includes: + - vendor/vojtech-dobes/phpstan-php-graphql-server/extension.neon + +graphql: + generatedDir: "" + schemas: + - "" +``` + +Next you have to tell the extension about your resolvers. If you're using framework integration, use corresponding package: + +- **Integration:** `vojtech-dobes/php-graphql-server-nette-integration` (for Nette Framework)
+ **Package:** [`vojtech-dobes/phpstan-php-graphql-server-nette-integration`](https://github.com/vojtech-dobes/phpstan-php-graphql-server-nette-integration) + +In case of custom setup, please implement `Vojtechdobes\PHPStan\GraphQL\Adapter` interface and register like this in `phpstan.neon`: + +```neon +services: + - class: MyCustomAdapter +``` diff --git a/src/GraphQL/Adapter.php b/src/GraphQL/Adapter.php new file mode 100644 index 0000000..eac947d --- /dev/null +++ b/src/GraphQL/Adapter.php @@ -0,0 +1,20 @@ + */ + public readonly array $schemas; + + + + /** + * @param list $schemas + * @throws Exception + */ + public function __construct( + public readonly string $generatedDir, + array $schemas, + ) + { + $this->schemas = array_map( + static function ($schema): string { + $schemaFile = realpath($schema); + + if ($schemaFile === false) { + throw new Exception("$schema doesn't exist"); + } + + return $schemaFile; + }, + $schemas, + ); + } + +} diff --git a/src/GraphQL/CorrespondanceRule.php b/src/GraphQL/CorrespondanceRule.php new file mode 100644 index 0000000..b9de753 --- /dev/null +++ b/src/GraphQL/CorrespondanceRule.php @@ -0,0 +1,370 @@ + + */ +final class CorrespondanceRule implements PHPStan\Rules\Rule +{ + + public function __construct( + private readonly Config $config, + private readonly PHPStan\Reflection\ReflectionProvider $reflectionProvider, + ) {} + + + + public function getNodeType(): string + { + return PHPStan\Node\CollectedDataNode::class; + } + + + + public function processNode(PhpParser\Node $node, PHPStan\Analyser\Scope $scope): array + { + $result = []; + + foreach ($this->config->schemas as $schemaName) { + $validClassName = Helpers::createValidSchemaClassName($schemaName); + $invalidClassName = Helpers::createInvalidSchemaClassName($schemaName); + + // following check bypasses PHPStan\Testing\RuleTestCase + // not being able to discover file generated on-the-fly + if ( + class_exists($invalidClassName) === false + && is_file($invalidClassNameFile = ($this->config->generatedDir . '/' . $invalidClassName . '.php')) + ) { + require_once $invalidClassNameFile; + } + + if ($this->reflectionProvider->hasClass($invalidClassName)) { + $result[] = PHPStan\Rules\RuleErrorBuilder::message("GraphQL schema isn't valid.") + ->identifier('graphql.schemaInvalid') + ->file($schemaName) + ->line(0) + ->nonIgnorable() + ->build(); + + continue; + } + + $schemaServiceOraculum = $this->createSchemaServiceOraculum($validClassName); + + $fields = $schemaServiceOraculum->listFields(); + + foreach ($fields as $field) { + [$objectType, $fieldName] = explode('.', $field); + + $schemaType = $schemaServiceOraculum->getFieldSchemaType($objectType, $fieldName); + + if ($schemaType[count($schemaType) - 1] === $schemaServiceOraculum->getRootOperationType(Vojtechdobes\GraphQL\OperationType::Query)) { + continue; + } + + $resolverClassType = $schemaServiceOraculum->getFieldResolverType($objectType, $fieldName); + + $expectedArgumentsType = $schemaServiceOraculum->getFieldArgumentsType($objectType, $fieldName); + $actualArgumentsType = $resolverClassType->getTemplateType(Vojtechdobes\GraphQL\FieldResolver::class, 'TArguments'); + + if ( + $actualArgumentsType->isNull()->yes() === false // misconfigured generics are already reported in native rule + && $expectedArgumentsType->isSuperTypeOf($actualArgumentsType)->yes() === false + ) { + $message = sprintf( + "Arguments %s of field %s aren't contravariant with arguments %s of resolver %s", + $expectedArgumentsType->describe(PHPStan\Type\VerbosityLevel::precise()), + $field, + $actualArgumentsType->describe(PHPStan\Type\VerbosityLevel::precise()), + $resolverClassType->getClassName(), + ); + + $result[] = PHPStan\Rules\RuleErrorBuilder::message($message) + ->identifier('graphql.argumentsMismatch') + ->file($schemaName) + ->build(); + } + + $result = [ + ...$result, + ...$this->listFieldResolvedValueErrors($scope, $schemaServiceOraculum, $schemaName, $objectType, $fieldName, $resolverClassType), + ]; + } + } + + return $result; + } + + + + private function createSchemaServiceOraculum(string $validClassName): SchemaServiceOraculum + { + // following check bypasses PHPStan\Testing\RuleTestCase + // not being able to discover file generated on-the-fly + if ( + class_exists($validClassName) === false + && is_file($validClassNameFile = ($this->config->generatedDir . '/' . $validClassName . '.php')) + ) { + require_once $validClassNameFile; + } + + return new SchemaServiceOraculum( + $this->reflectionProvider->getClass($validClassName), + ); + } + + + + /** + * @return list + */ + private function listFieldResolvedValueErrors( + PHPStan\Analyser\Scope $scope, + SchemaServiceOraculum $schemaServiceOraculum, + string $schemaName, + string $objectType, + string $fieldName, + PHPStan\Type\ObjectType $resolverClassType, + ): array + { + $result = []; + + $expectedPhpType = $schemaServiceOraculum->getFieldPhpType($objectType, $fieldName); + + [$actualPhpTypes, $errors] = $this->listFieldResolvedValueTypes( + $scope, + $schemaServiceOraculum, + $objectType, + $fieldName, + $resolverClassType, + ); + + foreach ($errors as $error) { + $result[] = PHPStan\Rules\RuleErrorBuilder::message($error) + ->identifier('graphql.typeMismatch') + ->file($schemaName) + ->build(); + } + + foreach ($actualPhpTypes as $actualPhpType) { + if ($expectedPhpType->isSuperTypeOf($actualPhpType)->yes() === false) { + $message = sprintf( + "Type of field %s should be %s but resolver %s returns %s", + "{$objectType}.{$fieldName}", + $expectedPhpType->describe(PHPStan\Type\VerbosityLevel::precise()), + $resolverClassType->describe(PHPStan\Type\VerbosityLevel::precise()), + $actualPhpType->describe(PHPStan\Type\VerbosityLevel::precise()), + ); + + $result[] = PHPStan\Rules\RuleErrorBuilder::message($message) + ->identifier('graphql.typeMismatch') + ->file($schemaName) + ->build(); + } + } + + return $result; + } + + + + /** + * @return array{list, list} + */ + private function listFieldResolvedValueTypes( + PHPStan\Analyser\Scope $scope, + SchemaServiceOraculum $schemaServiceOraculum, + string $objectType, + string $fieldName, + PHPStan\Type\ObjectType $resolverClassType, + ): array + { + $errors = []; + $types = []; + + if ($resolverClassType->getClassName() === Vojtechdobes\GraphQL\ArrayFieldResolver::class) { + $offsetType = new PHPStan\Type\Constant\ConstantStringType($fieldName); + + foreach ($this->listObjectTypeResolvedValueTypes($scope, $schemaServiceOraculum, $objectType) as $parentType) { + if ($parentType->isOffsetAccessible()->yes() === false) { + $errors[] = sprintf( + "Resolver %s of field %s expects parent to have array access, but parent is resolved to %s", + $resolverClassType->describe(PHPStan\Type\VerbosityLevel::precise()), + "{$objectType}.{$fieldName}", + $parentType->describe(PHPStan\Type\VerbosityLevel::precise()), + ); + } elseif ($parentType->hasOffsetValueType($offsetType)->yes() === false) { + $errors[] = sprintf( + "Resolver %s of field %s expects parent to have offset '%s', but parent is resolved to %s", + $resolverClassType->describe(PHPStan\Type\VerbosityLevel::precise()), + "{$objectType}.{$fieldName}", + $fieldName, + $parentType->describe(PHPStan\Type\VerbosityLevel::precise()), + ); + } else { + $types[] = $parentType->getOffsetValueType($offsetType); + } + } + } elseif ($resolverClassType->getClassName() === Vojtechdobes\GraphQL\GetterFieldResolver::class) { + $methodName = 'get' . ucfirst($fieldName); + + foreach ($this->listObjectTypeResolvedValueTypes($scope, $schemaServiceOraculum, $objectType) as $parentType) { + if ($parentType->isObject()->yes() === false) { + $errors[] = sprintf( + "Resolver %s of field %s expects parent to be an object, but parent is resolved to %s", + $resolverClassType->describe(PHPStan\Type\VerbosityLevel::precise()), + "{$objectType}.{$fieldName}", + $parentType->describe(PHPStan\Type\VerbosityLevel::precise()), + ); + } elseif ($parentType->hasMethod($methodName)->yes() === false) { + $errors[] = sprintf( + "Resolver %s of field %s expects parent to have method %s(), but method %s::%s() doesn't exist", + $resolverClassType->describe(PHPStan\Type\VerbosityLevel::precise()), + "{$objectType}.{$fieldName}", + $methodName, + $parentType->describe(PHPStan\Type\VerbosityLevel::precise()), + $methodName, + ); + } else { + $types[] = PHPStan\Type\TypeCombinator::union(...array_map( + static fn ($variant) => $variant->getReturnType(), + $parentType->getMethod($methodName, $scope)->getVariants(), + )); + } + } + } elseif ($resolverClassType->getClassName() === Vojtechdobes\GraphQL\PropertyFieldResolver::class) { + foreach ($this->listObjectTypeResolvedValueTypes($scope, $schemaServiceOraculum, $objectType) as $parentType) { + if ($parentType->isObject()->yes() === false) { + $errors[] = sprintf( + "Resolver %s of field %s expects parent to be an object, but parent is resolved to %s", + $resolverClassType->describe(PHPStan\Type\VerbosityLevel::precise()), + "{$objectType}.{$fieldName}", + $parentType->describe(PHPStan\Type\VerbosityLevel::precise()), + ); + } elseif ($parentType->hasProperty($fieldName)->yes() === false) { + $errors[] = sprintf( + "Resolver %s of field %s expects parent to have property \$%s, but property %s::\$%s doesn't exist", + $resolverClassType->describe(PHPStan\Type\VerbosityLevel::precise()), + "{$objectType}.{$fieldName}", + $fieldName, + $parentType->describe(PHPStan\Type\VerbosityLevel::precise()), + $fieldName, + ); + } else { + $types[] = $parentType->getProperty($fieldName, $scope)->getReadableType(); + } + } + } else { + $expectedParentType = $resolverClassType->getTemplateType(Vojtechdobes\GraphQL\FieldResolver::class, 'TObjectValue'); + + foreach ($this->listObjectTypeResolvedValueTypes($scope, $schemaServiceOraculum, $objectType) as $parentType) { + if ($parentType->isSuperTypeOf($expectedParentType)->yes() === false) { + $errors[] = sprintf( + "Resolver %s of field %s expects parent to be %s, but parent is resolved to %s", + $resolverClassType->describe(PHPStan\Type\VerbosityLevel::precise()), + "{$objectType}.{$fieldName}", + $expectedParentType->describe(PHPStan\Type\VerbosityLevel::precise()), + $parentType->describe(PHPStan\Type\VerbosityLevel::precise()), + ); + } + } + + if ($errors === []) { + $types[] = $resolverClassType->getTemplateType(Vojtechdobes\GraphQL\FieldResolver::class, 'TResolvedValue'); + } + } + + return [ + array_map( + fn ($type) => $this->stripAwayDeferred($type), + $types, + ), + $errors, + ]; + } + + + + /** + * @return list + */ + private function listObjectTypeResolvedValueTypes( + PHPStan\Analyser\Scope $scope, + SchemaServiceOraculum $schemaServiceOraculum, + string $objectType, + ): array + { + $result = []; + + foreach ($schemaServiceOraculum->listFieldsResolvedToObjectType($objectType) as [$parentObjectType, $parentFieldName]) { + [$parentTypes] = $this->listFieldResolvedValueTypes( + $scope, + $schemaServiceOraculum, + $parentObjectType, + $parentFieldName, + $schemaServiceOraculum->getFieldResolverType($parentObjectType, $parentFieldName), + ); + + foreach ($parentTypes as $parentType) { + $parentType = $this->unwrapType( + $schemaServiceOraculum, + $parentObjectType, + $parentFieldName, + $this->stripAwayDeferred($parentType), + ); + + $result[$parentType->describe(PHPStan\Type\VerbosityLevel::precise())] = $parentType; + } + } + + return array_values($result); + } + + + + private function stripAwayDeferred( + PHPStan\Type\Type $type, + ): PHPStan\Type\Type + { + return PHPStan\Type\TypeTraverser::map( + $type, + function (PHPStan\Type\Type $type, callable $traverse): PHPStan\Type\Type { + if ($type instanceof PHPStan\Type\Generic\GenericObjectType && $type->getClassName() === Vojtechdobes\GraphQL\Deferred::class) { + return $type->getTemplateType(Vojtechdobes\GraphQL\Deferred::class, 'TValue'); + } + + return $traverse($type); + }, + ); + } + + + + private function unwrapType( + SchemaServiceOraculum $schemaServiceOraculum, + string $objectType, + string $fieldName, + PHPStan\Type\Type $type, + ): PHPStan\Type\Type + { + $schemaType = $schemaServiceOraculum->getFieldSchemaType($objectType, $fieldName); + + foreach ($schemaType as $schemaTypeLevel) { + $type = PHPStan\Type\TypeCombinator::removeNull( + match ($schemaTypeLevel) { + ':list' => $type->getIterableValueType(), + default => $type, + }, + ); + } + + return $type; + } + +} diff --git a/src/GraphQL/Helpers.php b/src/GraphQL/Helpers.php new file mode 100644 index 0000000..a9d21ef --- /dev/null +++ b/src/GraphQL/Helpers.php @@ -0,0 +1,43 @@ +getSchema($schemaName); + } catch (Vojtechdobes\GraphQL\Exceptions\InvalidSchemaException $e) { + $this->generateInvalidSchemaClass( + $invalidClassName, + $e, + ); + + return; + } + + Nette\Utils\FileSystem::delete( + "{$this->generatedDir}/{$invalidClassName}.php", + ); + + $this->generateValidSchemaClass( + $validClassName, + $schema, + $adapter->getFieldResolverProvider($schemaName), + ); + } + + + + private function generateInvalidSchemaClass( + string $invalidClassName, + Vojtechdobes\GraphQL\Exceptions\InvalidSchemaException $e, + ): void + { + $file = new Nette\PhpGenerator\PhpFile(); + $file->setStrictTypes(); + + $file->addClass($invalidClassName); + + Nette\Utils\FileSystem::write( + "{$this->generatedDir}/{$invalidClassName}.php", + (string) $file, + ); + } + + + + private function generateValidSchemaClass( + string $className, + Vojtechdobes\GraphQL\TypeSystem\Schema $schema, + Vojtechdobes\GraphQL\FieldResolverProvider $fieldResolverProvider, + ): void + { + $file = new Nette\PhpGenerator\PhpFile(); + $file->setStrictTypes(); + + $class = $file->addClass($className); + + foreach ($schema->rootOperationTypes as $operation => $type) { + $class->addProperty(sprintf("root__%s__type", $operation)) + ->setPublic() + ->setValue($type); + } + + $fields = []; + $objectFields = []; + + foreach ($schema->getTypeDefinitions() as $typeDefinition) { + if (!$typeDefinition instanceof Vojtechdobes\GraphQL\TypeSystem\ObjectTypeDefinition) { + continue; + } + + if (str_starts_with($typeDefinition->name, '__')) { + continue; + } + + $objectType = $typeDefinition->name; + + foreach ($typeDefinition->fields as $fieldDefinition) { + $fieldName = $fieldDefinition->name; + + $fields[] = "{$objectType}.{$fieldName}"; + + $class->addProperty('field__' . $objectType . '_' . $fieldName . '__phpType') + ->setPublic() + ->addComment('@var ' . self::getPhpType($schema, $fieldDefinition->type)); + + $schemaType = []; + + $type = $fieldDefinition->type; + + while ($type !== null) { + $schemaType[] = match (true) { + $type instanceof Vojtechdobes\GraphQL\Types\ListType => ':list', + $type instanceof Vojtechdobes\GraphQL\Types\NamedType => $type->name, + $type instanceof Vojtechdobes\GraphQL\Types\NonNullType => ':nonNull', + default => throw new PHPStan\ShouldNotHappenException(), + }; + + $type = $type->getWrappedType(); + } + + $class->addProperty('field__' . $objectType . '_' . $fieldName . '__schemaType') + ->setPublic() + ->setValue($schemaType); + + $class->addProperty('field__' . $objectType . '_' . $fieldName . '__resolverClass') + ->setPublic() + ->addComment('@var ' . ( + $fieldResolverProvider->getFieldResolverClass("{$objectType}.{$fieldName}") + ?? $fieldResolverProvider->getFieldResolverClass($objectType) + )); + + $class->addProperty('field__' . $objectType . '_' . $fieldName . '__arguments') + ->setPublic() + ->addComment('@var ' . sprintf( + 'array{%s}', + implode(', ', array_map( + fn ($argumentDefinition) => sprintf( + '%s: %s', + $argumentDefinition->name, + self::getPhpType( + $schema, + $argumentDefinition->type, + ), + ), + $fieldDefinition->argumentDefinitions, + )), + )); + + $typeDefinition = $schema->getTypeDefinition($fieldDefinition->type->getNamedType()); + + if ($typeDefinition instanceof Vojtechdobes\GraphQL\TypeSystem\ObjectTypeDefinition) { + $objectFields[$typeDefinition->name] ??= []; + $objectFields[$typeDefinition->name][] = [$objectType, $fieldName]; + } + } + } + + $class->addProperty('fields') + ->setPublic() + ->setValue($fields); + + foreach ($objectFields as $objectType => $fieldNames) { + $class->addProperty('objectType__' . $objectType .'__fields') + ->setPublic() + ->setValue($fieldNames); + } + + Nette\Utils\FileSystem::write( + "{$this->generatedDir}/{$className}.php", + (string) $file, + ); + } + + + + private static function getPhpType( + Vojtechdobes\GraphQL\TypeSystem\Schema $schema, + Vojtechdobes\GraphQL\Types\Type $type, + ): string + { + return match (true) { + $type instanceof Vojtechdobes\GraphQL\Types\ListType => 'iterable<' . self::getPhpType($schema, $type->itemType) . '>|null', + $type instanceof Vojtechdobes\GraphQL\Types\NamedType => self::getPhpNamedType($schema, $type), + $type instanceof Vojtechdobes\GraphQL\Types\NonNullType => substr(self::getPhpType($schema, $type->type), 0, -5), // remove |null + default => throw new PHPStan\ShouldNotHappenException(), + }; + } + + + + private static function getPhpNamedType( + Vojtechdobes\GraphQL\TypeSystem\Schema $schema, + Vojtechdobes\GraphQL\Types\NamedType $type, + ): string + { + $typeDefinition = $schema->getTypeDefinitionOrNull($type->name); + + if ($typeDefinition === null) { + throw new PHPStan\ShouldNotHappenException("Type definition '{$type->name}' can't be found"); + } + + if ($typeDefinition instanceof Vojtechdobes\GraphQL\TypeSystem\ScalarTypeDefinition) { + $scalarFormatter = $schema->scalarImplementationRegistry->getItem($typeDefinition->name); + + $fieldType = '__GraphQL__Scalar<' . $scalarFormatter::class. '>'; + } elseif ($typeDefinition instanceof Vojtechdobes\GraphQL\TypeSystem\EnumTypeDefinition) { + $enumClass = $schema->getEnumClass($typeDefinition->name); + + if ($enumClass !== null) { + $fieldType = $enumClass; + } else { + $fieldType = implode('|', array_map( + static fn ($enumValueDefinition) => '"' . $enumValueDefinition->name . '"', + $typeDefinition->enumValues, + )); + } + } elseif ($typeDefinition instanceof Vojtechdobes\GraphQL\TypeSystem\InputObjectTypeDefinition) { + $inputFieldTypes = []; + + foreach ($typeDefinition->fields as $inputFieldDefinition) { + $inputFieldTypes[] = sprintf( + '%s: %s', + $inputFieldDefinition->name, + self::getPhpType( + $schema, + $inputFieldDefinition->type, + ), + ); + } + + $fieldType = sprintf( + 'array{%s}', + implode(', ', $inputFieldTypes), + ); + } else { + $fieldType = 'mixed'; + } + + return $fieldType . '|null'; + } + +} diff --git a/src/GraphQL/SchemaServiceOraculum.php b/src/GraphQL/SchemaServiceOraculum.php new file mode 100644 index 0000000..5da2d5b --- /dev/null +++ b/src/GraphQL/SchemaServiceOraculum.php @@ -0,0 +1,126 @@ +schemaReflection + ->getNativeProperty('root__' . $operationType->value . '__type') + ->getNativeReflection() + ->getDefaultValue(); + } + + + + /** + * @return list + */ + public function listFields(): array + { + return $this->schemaReflection + ->getNativeProperty('fields') + ->getNativeReflection() + ->getDefaultValue(); + } + + + + /** + * @return list + */ + public function listFieldsResolvedToObjectType(string $objectType): array + { + $propertyName = 'objectType__' . $objectType . '__fields'; + + if ($this->schemaReflection->hasProperty($propertyName) === false) { + return []; + } + + return $this->schemaReflection + ->getNativeProperty($propertyName) + ->getNativeReflection() + ->getDefaultValue(); + } + + + + /** + * @return list + */ + public function getFieldSchemaType(string $objectType, string $fieldName): array + { + $propertyName = 'field__' . $objectType . '_' . $fieldName . '__schemaType'; + + if ($this->schemaReflection->hasProperty($propertyName) === false) { + return []; + } + + return $this->schemaReflection + ->getNativeProperty($propertyName) + ->getNativeReflection() + ->getDefaultValue(); + } + + + + public function getFieldResolverType(string $objectType, string $fieldName): PHPStan\Type\ObjectType + { + return $this->schemaReflection + ->getNativeProperty('field__' . $objectType . '_' . $fieldName . '__resolverClass') + ->getPhpDocType(); + } + + + + public function getFieldPhpType(string $objectType, string $fieldName): PHPStan\Type\Type + { + return $this->resolveEncodedPhpType( + $this->schemaReflection + ->getNativeProperty('field__' . $objectType . '_' . $fieldName . '__phpType') + ->getPhpDocType() + ); + } + + + + public function getFieldArgumentsType(string $objectType, string $fieldName): PHPStan\Type\Type + { + return $this->resolveEncodedPhpType( + $this->schemaReflection + ->getNativeProperty('field__' . $objectType . '_' . $fieldName . '__arguments') + ->getPhpDocType() + ); + } + + + + private function resolveEncodedPhpType(PHPStan\Type\Type $encodedType): PHPStan\Type\Type + { + return PHPStan\Type\TypeTraverser::map( + $encodedType, + function (PHPStan\Type\Type $type, callable $traverse): PHPStan\Type\Type { + if ($type instanceof PHPStan\Type\Generic\GenericObjectType && $type->getClassName() === '__GraphQL__Scalar') { + return $type + ->getTypes()[0] + ->getTemplateType(Vojtechdobes\GraphQL\ScalarImplementation::class, 'TValue'); + } + + return $traverse($type); + }, + ); + } + +} diff --git a/src/GraphQL/bootstrap.php b/src/GraphQL/bootstrap.php new file mode 100644 index 0000000..4629ce7 --- /dev/null +++ b/src/GraphQL/bootstrap.php @@ -0,0 +1,20 @@ +getByType(Vojtechdobes\PHPStan\GraphQL\Config::class); + +$schemaClassGenerator = new Vojtechdobes\PHPStan\GraphQL\SchemaClassGenerator( + $config->generatedDir, +); + +foreach ($config->schemas as $schemaName) { + $schemaClassGenerator->generateSchemaClass( + $schemaName, + Vojtechdobes\PHPStan\GraphQL\Helpers::createValidSchemaClassName($schemaName), + Vojtechdobes\PHPStan\GraphQL\Helpers::createInvalidSchemaClassName($schemaName), + $container->getByType(Vojtechdobes\PHPStan\GraphQL\Adapter::class), + ); +} diff --git a/tests-shared/AbstractCorrespondanceRuleTest.php b/tests-shared/AbstractCorrespondanceRuleTest.php new file mode 100644 index 0000000..381d2bc --- /dev/null +++ b/tests-shared/AbstractCorrespondanceRuleTest.php @@ -0,0 +1,61 @@ + + */ +abstract class AbstractCorrespondanceRuleTest extends PHPStan\Testing\RuleTestCase +{ + + final protected function getRule(): PHPStan\Rules\Rule + { + return self::getContainer()->getByType(Vojtechdobes\PHPStan\GraphQL\CorrespondanceRule::class); + } + + + + final protected function getCollectors(): array + { + // rule based on CollectedDataNode won't run without any collector + return [ + new DummyCollector(), + ]; + } + + + + final public function testRule(): void + { + $this->analyse([__DIR__ . '/DummyCollector.php'], [ + [ + "Type of field Query.invalidStringResolvedAsBool should be string|null but resolver Vojtechdobes\TestsShared\Resolvers\QueryInvalidStringResolvedAsBoolFieldResolver returns bool", + -1, + ], + [ + "Arguments array{arg1: string|null} of field Query.invalidArgumentsMismatch aren't contravariant with arguments array{} of resolver Vojtechdobes\TestsShared\Resolvers\QueryInvalidArgumentsMismatchFieldResolver", + -1, + ], + [ + "Resolver Vojtechdobes\TestsShared\Resolvers\InvalidParentTypeNameFieldResolver of field InvalidParentType.name expects parent to be Vojtechdobes\TestsShared\Resolvers\Person, but parent is resolved to array{}", + -1, + ], + ]); + } + + + + final public static function getAdditionalConfigFiles(): array + { + return [static::getTestConfigFile()]; + } + + + + abstract public static function getTestConfigFile(): string; + +} diff --git a/tests-shared/DummyCollector.php b/tests-shared/DummyCollector.php new file mode 100644 index 0000000..b32563d --- /dev/null +++ b/tests-shared/DummyCollector.php @@ -0,0 +1,27 @@ + + */ +final class DummyCollector implements PHPStan\Collectors\Collector +{ + + public function getNodeType(): string + { + return PHPStan\Node\FileNode::class; + } + + + + public function processNode(PhpParser\Node $node, PHPStan\Analyser\Scope $scope) + { + return true; + } + +} diff --git a/tests-shared/Resolvers/InvalidParentTypeNameFieldResolver.php b/tests-shared/Resolvers/InvalidParentTypeNameFieldResolver.php new file mode 100644 index 0000000..7754231 --- /dev/null +++ b/tests-shared/Resolvers/InvalidParentTypeNameFieldResolver.php @@ -0,0 +1,19 @@ + + */ +final class InvalidParentTypeNameFieldResolver implements Vojtechdobes\GraphQL\FieldResolver +{ + + public function resolveField(mixed $objectValue, Vojtechdobes\GraphQL\FieldSelection $field): mixed + { + return $objectValue->name; + } + +} diff --git a/tests-shared/Resolvers/ObjectType.php b/tests-shared/Resolvers/ObjectType.php new file mode 100644 index 0000000..73562c2 --- /dev/null +++ b/tests-shared/Resolvers/ObjectType.php @@ -0,0 +1,20 @@ + + */ +final class QueryArrayTypeFieldResolver implements Vojtechdobes\GraphQL\FieldResolver +{ + + public function resolveField(mixed $objectValue, Vojtechdobes\GraphQL\FieldSelection $field): mixed + { + return [ + 'a' => 'Alice', + ]; + } + +} diff --git a/tests-shared/Resolvers/QueryInvalidArgumentsMismatchFieldResolver.php b/tests-shared/Resolvers/QueryInvalidArgumentsMismatchFieldResolver.php new file mode 100644 index 0000000..728d0d4 --- /dev/null +++ b/tests-shared/Resolvers/QueryInvalidArgumentsMismatchFieldResolver.php @@ -0,0 +1,19 @@ + + */ +final class QueryInvalidArgumentsMismatchFieldResolver implements Vojtechdobes\GraphQL\FieldResolver +{ + + public function resolveField(mixed $objectValue, Vojtechdobes\GraphQL\FieldSelection $field): mixed + { + return 'Charlie'; + } + +} diff --git a/tests-shared/Resolvers/QueryInvalidStringResolvedAsBoolFieldResolver.php b/tests-shared/Resolvers/QueryInvalidStringResolvedAsBoolFieldResolver.php new file mode 100644 index 0000000..9e93e43 --- /dev/null +++ b/tests-shared/Resolvers/QueryInvalidStringResolvedAsBoolFieldResolver.php @@ -0,0 +1,19 @@ + + */ +final class QueryInvalidStringResolvedAsBoolFieldResolver implements Vojtechdobes\GraphQL\FieldResolver +{ + + public function resolveField(mixed $objectValue, Vojtechdobes\GraphQL\FieldSelection $field): mixed + { + return true; + } + +} diff --git a/tests-shared/Resolvers/QueryObjectTypeFieldResolver.php b/tests-shared/Resolvers/QueryObjectTypeFieldResolver.php new file mode 100644 index 0000000..a9589e4 --- /dev/null +++ b/tests-shared/Resolvers/QueryObjectTypeFieldResolver.php @@ -0,0 +1,19 @@ + + */ +final class QueryObjectTypeFieldResolver implements Vojtechdobes\GraphQL\FieldResolver +{ + + public function resolveField(mixed $objectValue, Vojtechdobes\GraphQL\FieldSelection $field): mixed + { + return new ObjectType(); + } + +} diff --git a/tests-shared/Resolvers/QueryProviderOfInvalidParentTypeFieldResolver.php b/tests-shared/Resolvers/QueryProviderOfInvalidParentTypeFieldResolver.php new file mode 100644 index 0000000..6025872 --- /dev/null +++ b/tests-shared/Resolvers/QueryProviderOfInvalidParentTypeFieldResolver.php @@ -0,0 +1,19 @@ + + */ +final class QueryProviderOfInvalidParentTypeFieldResolver implements Vojtechdobes\GraphQL\FieldResolver +{ + + public function resolveField(mixed $objectValue, Vojtechdobes\GraphQL\FieldSelection $field): mixed + { + return []; + } + +} diff --git a/tests-shared/Resolvers/QueryValidDeferredFieldResolver.php b/tests-shared/Resolvers/QueryValidDeferredFieldResolver.php new file mode 100644 index 0000000..592478a --- /dev/null +++ b/tests-shared/Resolvers/QueryValidDeferredFieldResolver.php @@ -0,0 +1,19 @@ +> + */ +final class QueryValidDeferredFieldResolver implements Vojtechdobes\GraphQL\FieldResolver +{ + + public function resolveField(mixed $objectValue, Vojtechdobes\GraphQL\FieldSelection $field): mixed + { + return new Vojtechdobes\GraphQL\Deferred(static fn () => 'David'); + } + +} diff --git a/tests-shared/Resolvers/QueryValidNonNullStringFieldResolver.php b/tests-shared/Resolvers/QueryValidNonNullStringFieldResolver.php new file mode 100644 index 0000000..53b22d9 --- /dev/null +++ b/tests-shared/Resolvers/QueryValidNonNullStringFieldResolver.php @@ -0,0 +1,19 @@ + + */ +final class QueryValidNonNullStringFieldResolver implements Vojtechdobes\GraphQL\FieldResolver +{ + + public function resolveField(mixed $objectValue, Vojtechdobes\GraphQL\FieldSelection $field): mixed + { + return 'Alice'; + } + +} diff --git a/tests-shared/schema.graphqls b/tests-shared/schema.graphqls new file mode 100644 index 0000000..4d44dec --- /dev/null +++ b/tests-shared/schema.graphqls @@ -0,0 +1,26 @@ +type Query { + validNonNullString: String! + invalidStringResolvedAsBool: String + + invalidArgumentsMismatch(arg1: String): String + + validDeferred: String + + arrayType: ArrayType! + objectType: ObjectType! + + providerOfInvalidParentType: InvalidParentType! +} + +type ArrayType { + a: String! +} + +type InvalidParentType { + name: String! +} + +type ObjectType { + withGetter: String! + withProperty: String! +} diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..eb5bd07 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +generated-graphql diff --git a/tests/CorrespondanceRuleTest.extension.neon b/tests/CorrespondanceRuleTest.extension.neon new file mode 100644 index 0000000..c509c36 --- /dev/null +++ b/tests/CorrespondanceRuleTest.extension.neon @@ -0,0 +1,10 @@ +includes: + - ../extension.neon + +parameters: + graphql: + generatedDir: %rootDir%/../../../tests/generated-graphql + schemas: [%rootDir%/../../../tests-shared/schema.graphqls] + +services: + - Vojtechdobes\Tests\CustomAdapter diff --git a/tests/CorrespondanceRuleTest.php b/tests/CorrespondanceRuleTest.php new file mode 100644 index 0000000..9dbfa90 --- /dev/null +++ b/tests/CorrespondanceRuleTest.php @@ -0,0 +1,16 @@ +loadSchema( + schemaPath: __DIR__ . '/../tests-shared/schema.graphqls', + enumClasses: [], + scalarImplementations: [], + ); + } + + + + public function getFieldResolverProvider(string $schemaName): Vojtechdobes\GraphQL\FieldResolverProvider + { + return new Vojtechdobes\GraphQL\StaticFieldResolverProvider([ + 'Query.validNonNullString' => new Vojtechdobes\TestsShared\Resolvers\QueryValidNonNullStringFieldResolver(), + 'Query.invalidStringResolvedAsBool' => new Vojtechdobes\TestsShared\Resolvers\QueryInvalidStringResolvedAsBoolFieldResolver(), + 'Query.invalidArgumentsMismatch' => new Vojtechdobes\TestsShared\Resolvers\QueryInvalidArgumentsMismatchFieldResolver(), + 'Query.validDeferred' => new Vojtechdobes\TestsShared\Resolvers\QueryValidDeferredFieldResolver(), + + 'Query.arrayType' => new Vojtechdobes\TestsShared\Resolvers\QueryArrayTypeFieldResolver(), + 'ArrayType' => new Vojtechdobes\GraphQL\ArrayFieldResolver(), + + 'Query.objectType' => new Vojtechdobes\TestsShared\Resolvers\QueryObjectTypeFieldResolver(), + 'ObjectType.withGetter' => new Vojtechdobes\GraphQL\GetterFieldResolver(), + 'ObjectType.withProperty' => new Vojtechdobes\GraphQL\PropertyFieldResolver(), + + 'Query.providerOfInvalidParentType' => new Vojtechdobes\TestsShared\Resolvers\QueryProviderOfInvalidParentTypeFieldResolver(), + 'InvalidParentType.name' => new Vojtechdobes\TestsShared\Resolvers\InvalidParentTypeNameFieldResolver(), + ]); + } + +}