├── .codecov.yml ├── .github └── workflows │ ├── autoformat.yml │ └── validate.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── BigInt.php ├── Date.php ├── DateScalar.php ├── DateTime.php ├── DateTimeTz.php ├── Email.php ├── IntRange.php ├── JSON.php ├── MixedScalar.php ├── NullScalar.php ├── Regex.php ├── StringScalar.php └── Utils.php /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | only_pulls: true 6 | comment: false 7 | -------------------------------------------------------------------------------- /.github/workflows/autoformat.yml: -------------------------------------------------------------------------------- 1 | name: "Autoformat" 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | 7 | jobs: 8 | composer-normalize: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | ref: ${{ github.head_ref }} 14 | 15 | - uses: shivammathur/setup-php@v2 16 | with: 17 | coverage: none 18 | extensions: mbstring 19 | php-version: 8.1 20 | 21 | - run: composer install --no-interaction --no-progress --no-suggest 22 | 23 | - run: composer normalize 24 | 25 | - uses: stefanzweifel/git-auto-commit-action@v4 26 | with: 27 | commit_message: Normalize composer.json 28 | 29 | prettier: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | with: 34 | ref: ${{ github.head_ref }} 35 | 36 | - uses: creyD/prettier_action@v4.3 37 | with: 38 | prettier_options: --write --tab-width=2 *.md **/*.md 39 | branch: ${{ github.head_ref }} 40 | commit_message: Prettify docs 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | 44 | php-cs-fixer: 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v4 48 | with: 49 | ref: ${{ github.head_ref }} 50 | 51 | - uses: shivammathur/setup-php@v2 52 | with: 53 | coverage: none 54 | extensions: mbstring 55 | php-version: 8.1 56 | 57 | - run: composer install --no-interaction --no-progress --no-suggest 58 | 59 | - run: vendor/bin/php-cs-fixer fix 60 | 61 | - run: git pull 62 | 63 | - uses: stefanzweifel/git-auto-commit-action@v4 64 | with: 65 | commit_message: Apply php-cs-fixer changes 66 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/categories/automating-your-workflow-with-github-actions 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | name: "Validate" 10 | 11 | jobs: 12 | composer-validate: 13 | name: "Validate composer dependencies" 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: "Checkout" 19 | uses: actions/checkout@master 20 | 21 | - name: "Install PHP with extensions" 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | coverage: none 25 | extensions: mbstring 26 | php-version: 8.1 27 | 28 | - name: "Validate composer.json and composer.lock" 29 | run: composer validate --strict 30 | 31 | static-code-analysis: 32 | name: "Static Code Analysis" 33 | 34 | runs-on: ubuntu-latest 35 | 36 | steps: 37 | - name: "Checkout" 38 | uses: actions/checkout@v4 39 | 40 | - name: "Install PHP with extensions" 41 | uses: shivammathur/setup-php@v2 42 | with: 43 | coverage: none 44 | php-version: 8.0 45 | 46 | - name: "Install dependencies with composer" 47 | run: composer install --no-interaction --no-progress 48 | 49 | - name: "Run static analysis with phpstan" 50 | run: vendor/bin/phpstan 51 | 52 | tests: 53 | name: Test for PHP ${{ matrix.php-version }} (${{ matrix.dependencies }}) 54 | 55 | runs-on: ubuntu-latest 56 | 57 | strategy: 58 | matrix: 59 | php-version: 60 | - "8.0" 61 | - "8.1" 62 | - "8.2" 63 | - "8.3" 64 | - "8.4" 65 | dependencies: 66 | - "prefer-lowest" 67 | - "prefer-stable" 68 | 69 | steps: 70 | - name: "Checkout" 71 | uses: actions/checkout@v4 72 | 73 | - name: "Install PHP with extensions" 74 | uses: shivammathur/setup-php@v2 75 | with: 76 | coverage: none 77 | php-version: ${{ matrix.php-version }} 78 | 79 | - name: "Install dependencies with composer" 80 | run: composer update --${{ matrix.dependencies }} --no-interaction --no-progress 81 | 82 | - name: "Run unit tests with phpunit" 83 | run: vendor/bin/phpunit 84 | 85 | code-coverage: 86 | name: "Code Coverage" 87 | 88 | runs-on: ubuntu-latest 89 | 90 | steps: 91 | - name: "Checkout" 92 | uses: actions/checkout@v4 93 | 94 | - name: "Install PHP with extensions" 95 | uses: shivammathur/setup-php@v2 96 | with: 97 | coverage: pcov 98 | php-version: 8.1 99 | 100 | - name: "Install dependencies with composer" 101 | run: composer install --no-interaction --no-progress 102 | 103 | - name: "Collect code coverage" 104 | run: vendor/bin/phpunit --coverage-clover=coverage.xml 105 | 106 | - name: "Send code coverage report to codecov.io" 107 | uses: codecov/codecov-action@v2 108 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | [See GitHub Releases](https://github.com/mll-lab/graphql-php-scalars/releases). 9 | 10 | ## Unreleased 11 | 12 | ## v6.4.0 13 | 14 | ### Added 15 | 16 | - Support `thecodingmachine/safe` 3 17 | 18 | ## v6.3.0 19 | 20 | ### Added 21 | 22 | - Add a new abstract scalar `IntRange` 23 | 24 | ## v6.2.0 25 | 26 | ### Added 27 | 28 | - Add a new scalar `BigInt` 29 | 30 | ## v6.1.0 31 | 32 | ### Added 33 | 34 | - Support `egulias/email-validator:^4` 35 | 36 | ## v6.0.0 37 | 38 | ### Changed 39 | 40 | - Require `webonyx/graphql-php:^15` 41 | 42 | ### Removed 43 | 44 | - Drop support for PHP 7.4 45 | 46 | ## v5.4.1 47 | 48 | ### Changed 49 | 50 | - Clean up error messages 51 | 52 | ## v5.4.0 53 | 54 | ### Added 55 | 56 | - Support `spatie/regex` versions 2 and 3 57 | 58 | ## v5.3.0 59 | 60 | ### Added 61 | 62 | - Support `thecodingmachine/safe` version `^2` 63 | 64 | ## v5.2.0 65 | 66 | ### Added 67 | 68 | - Add scalars `Date`, `DateTime` and `DateTimeTz` 69 | 70 | ## v5.1.0 71 | 72 | ### Added 73 | 74 | - Add scalar `Null` 75 | 76 | ## v5.0.1 77 | 78 | ### Fixed 79 | 80 | - Allow `egulias/email-validator:^2` 81 | 82 | ## v5.0.0 83 | 84 | ### Fixed 85 | 86 | - Return coerced string value in `Regex::parseValue()` 87 | 88 | ### Changed 89 | 90 | - Require `egulias/email-validator:^3` 91 | 92 | ### Removed 93 | 94 | - Drop support for PHP 7.2 and 7.3 95 | 96 | ## v4.1.1 97 | 98 | ### Fixed 99 | 100 | - Move `ext-json` to `require` section in `composer.json` 101 | 102 | ## v4.1.0 103 | 104 | ### Changed 105 | 106 | - Improve error message when values can not be coerced into strings 107 | 108 | ## v4.0.0 109 | 110 | ### Added 111 | 112 | - Support PHP 8 113 | 114 | ### Changed 115 | 116 | - Rename `Mixed` class to `MixedScalar` because `mixed` is a reserved name in PHP 8. 117 | The GraphQL name of the scalar is still `Mixed` so the schema does not change. 118 | 119 | ## v3.1.0 120 | 121 | ### Added 122 | 123 | - Support `webonyx/graphql-php@^14.0.0` 124 | 125 | ## v3.0.2 126 | 127 | ### Changed 128 | 129 | - Move util functions to class for better autoloading 130 | 131 | ## v3.0.1 132 | 133 | ### Fixed 134 | 135 | - Export only minimally needed files in distribution package 136 | 137 | ## v3.0.0 138 | 139 | ### Changed 140 | 141 | - Bump dependencies of various packages 142 | 143 | ### Removed 144 | 145 | - Remove support for PHP 7.1 146 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 MLL Münchner Leukämielabor GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-php-scalars 2 | 3 | A collection of custom scalar types for usage with https://github.com/webonyx/graphql-php 4 | 5 | [![Validate](https://github.com/mll-lab/graphql-php-scalars/workflows/Validate/badge.svg)](https://github.com/mll-lab/graphql-php-scalars/actions) 6 | [![codecov](https://codecov.io/gh/mll-lab/graphql-php-scalars/branch/master/graph/badge.svg)](https://codecov.io/gh/mll-lab/graphql-php-scalars) 7 | 8 | [![GitHub license](https://img.shields.io/github/license/mll-lab/graphql-php-scalars.svg)](https://github.com/mll-lab/graphql-php-scalars/blob/master/LICENSE) 9 | [![Packagist](https://img.shields.io/packagist/v/mll-lab/graphql-php-scalars.svg)](https://packagist.org/packages/mll-lab/graphql-php-scalars) 10 | [![Packagist](https://img.shields.io/packagist/dt/mll-lab/graphql-php-scalars.svg)](https://packagist.org/packages/mll-lab/graphql-php-scalars) 11 | 12 | ## Installation 13 | 14 | ```sh 15 | composer require mll-lab/graphql-php-scalars 16 | ``` 17 | 18 | ## Usage 19 | 20 | You can use the provided Scalars just like any other type in your schema definition. 21 | Check [SchemaUsageTest](tests/SchemaUsageTest.php) for an example. 22 | 23 | ### [BigInt](src/BigInt.php) 24 | 25 | An arbitrarily long sequence of digits that represents a big integer. 26 | 27 | ### [Date](src/Date.php) 28 | 29 | A date string with format `Y-m-d`, e.g. `2011-05-23`. 30 | 31 | The following conversion applies to all date scalars: 32 | 33 | - Outgoing values can either be valid date strings or `\DateTimeInterface` instances. 34 | - Incoming values must always be valid date strings and will be converted to `\DateTimeImmutable` instances. 35 | 36 | ### [DateTime](src/DateTime.php) 37 | 38 | A datetime string with format `Y-m-d H:i:s`, e.g. `2018-05-23 13:43:32`. 39 | 40 | ### [DateTimeTz](src/DateTimeTz.php) 41 | 42 | A datetime string with format `Y-m-d\TH:i:s.uP`, e.g. `2020-04-20T16:20:04+04:00`, `2020-04-20T16:20:04Z`. 43 | 44 | ### [Email](src/Email.php) 45 | 46 | A [RFC 5321](https://tools.ietf.org/html/rfc5321) compliant email. 47 | 48 | ### [IntRange](src/IntRange.php) 49 | 50 | Allows defining numeric scalars where the values must lie between a defined minimum and maximum. 51 | 52 | ```php 53 | use MLL\GraphQLScalars\IntRange; 54 | 55 | final class UpToADozen extends IntRange 56 | { 57 | protected static function min(): int 58 | { 59 | return 1; 60 | } 61 | 62 | protected static function max(): int 63 | { 64 | return 12; 65 | } 66 | } 67 | ``` 68 | 69 | ### [JSON](src/JSON.php) 70 | 71 | Arbitrary data encoded in JavaScript Object Notation. See https://www.json.org. 72 | 73 | This expects a string in JSON format, not an arbitrary JSON value or GraphQL literal. 74 | 75 | ```graphql 76 | type Query { 77 | foo(bar: JSON!): JSON! 78 | } 79 | 80 | # Wrong, the given value is a GraphQL literal object 81 | { 82 | foo(bar: { baz: 2 }) 83 | } 84 | 85 | # Correct, the given value is a JSON string representing an object 86 | { 87 | foo(bar: "{ \"bar\": 2 }") 88 | } 89 | ``` 90 | 91 | ```json 92 | // Wrong, the variable value is a JSON object 93 | { 94 | "query": "query ($bar: JSON!) { foo(bar: $bar) }", 95 | "variables": { 96 | "bar": { 97 | "baz": 2 98 | } 99 | } 100 | } 101 | 102 | // Correct, the variable value is a JSON string representing an object 103 | { 104 | "query": "query ($bar: JSON!) { foo(bar: $bar) }", 105 | "variables": { 106 | "bar": "{ \"bar\": 2 }" 107 | } 108 | } 109 | ``` 110 | 111 | JSON responses will contain nested JSON strings. 112 | 113 | ```json 114 | { 115 | "data": { 116 | "foo": "{ \"bar\": 2 }" 117 | } 118 | } 119 | ``` 120 | 121 | ### [Mixed](src/MixedScalar.php) 122 | 123 | Loose type that allows any value. Be careful when passing in large `Int` or `Float` literals, 124 | as they may not be parsed correctly on the server side. Use `String` literals if you are 125 | dealing with really large numbers to be on the safe side. 126 | 127 | ### [Null](src/NullScalar.php) 128 | 129 | Always `null`. Strictly validates value is non-null, no coercion. 130 | 131 | ### [Regex](src/Regex.php) 132 | 133 | The `Regex` class allows you to define a custom scalar that validates that the given 134 | value matches a regular expression. 135 | 136 | The quickest way to define a custom scalar is the `make` factory method. Just provide 137 | a name and a regular expression, you will receive a ready-to-use custom regex scalar. 138 | 139 | ```php 140 | use MLL\GraphQLScalars\Regex; 141 | 142 | $hexValue = Regex::make( 143 | 'HexValue', 144 | 'A hexadecimal color is specified with: `#RRGGBB`, where `RR` (red), `GG` (green) and `BB` (blue) are hexadecimal integers between `00` and `FF` specifying the intensity of the color.', 145 | '/^#?([a-f0-9]{6}|[a-f0-9]{3})$/' 146 | ); 147 | ``` 148 | 149 | You may also define your regex scalar as a class. 150 | 151 | ```php 152 | use MLL\GraphQLScalars\Regex; 153 | 154 | // The name is implicitly set through the class name here 155 | class HexValue extends Regex 156 | { 157 | /** The description that is used for schema introspection. */ 158 | public ?string $description = /** @lang Markdown */<<<'MARKDOWN' 159 | A hexadecimal color is specified with: `#RRGGBB`, where `RR` (red), `GG` (green) and `BB` (blue) 160 | are hexadecimal integers between `00` and `FF` specifying the intensity of the color. 161 | MARKDOWN; 162 | 163 | public static function regex(): string 164 | { 165 | return '/^#?([a-f0-9]{6}|[a-f0-9]{3})$/'; 166 | } 167 | } 168 | ``` 169 | 170 | ### [StringScalar](src/StringScalar.php) 171 | 172 | The `StringScalar` encapsulates all the boilerplate associated with creating a string-based Scalar type. 173 | It performs basic checks and coercion, you can focus on the minimal logic that is specific to your use case. 174 | 175 | All you have to specify is a function that checks if the given string is valid. 176 | Use the factory method `make` to generate an instance on the fly. 177 | 178 | ```php 179 | use MLL\GraphQLScalars\StringScalar; 180 | 181 | $coolName = StringScalar::make( 182 | 'CoolName', 183 | 'A name that is most definitely cool.', 184 | static fn (string $name): bool => in_array($name, [ 185 | 'Vladar', 186 | 'Benedikt', 187 | 'Christopher', 188 | ]), 189 | ); 190 | ``` 191 | 192 | Or you may simply extend the class, check out the implementation of the [Email](src/Email.php) scalar to see how. 193 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mll-lab/graphql-php-scalars", 3 | "description": "A collection of custom scalar types for usage with https://github.com/webonyx/graphql-php", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "graphql", 8 | "php" 9 | ], 10 | "authors": [ 11 | { 12 | "name": "Benedikt Franke", 13 | "email": "benedikt@franke.tech" 14 | } 15 | ], 16 | "require": { 17 | "php": "^8", 18 | "ext-json": "*", 19 | "egulias/email-validator": "^2.1.17 || ^3 || ^4", 20 | "spatie/regex": "^1.4 || ^2 || ^3", 21 | "thecodingmachine/safe": "^1.3 || ^2 || ^3", 22 | "webonyx/graphql-php": "^15" 23 | }, 24 | "require-dev": { 25 | "ergebnis/composer-normalize": "^2.16", 26 | "mll-lab/php-cs-fixer-config": "^5", 27 | "phpstan/extension-installer": "^1", 28 | "phpstan/phpstan": "^1", 29 | "phpstan/phpstan-deprecation-rules": "^1", 30 | "phpstan/phpstan-phpunit": "^1", 31 | "phpstan/phpstan-strict-rules": "^1", 32 | "phpunit/phpunit": "^9 || ^10", 33 | "symfony/var-dumper": "^5.4 || ^6", 34 | "thecodingmachine/phpstan-safe-rule": "^1.1" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "MLL\\GraphQLScalars\\": "src" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "MLL\\GraphQLScalars\\Tests\\": "tests/" 44 | } 45 | }, 46 | "config": { 47 | "allow-plugins": { 48 | "ergebnis/composer-normalize": true, 49 | "phpstan/extension-installer": true 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/BigInt.php: -------------------------------------------------------------------------------- 1 | \d{4}-(0[1-9]|1[012])-(0[1-9]|[12][\d]|3[01]))$~'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/DateScalar.php: -------------------------------------------------------------------------------- 1 | tryParsingDate($value, InvariantViolation::class); 19 | } 20 | 21 | return $value->format(static::outputFormat()); 22 | } 23 | 24 | public function parseValue($value): \DateTimeInterface 25 | { 26 | return $this->tryParsingDate($value, Error::class); 27 | } 28 | 29 | public function parseLiteral($valueNode, ?array $variables = null): \DateTimeInterface 30 | { 31 | if (! $valueNode instanceof StringValueNode) { 32 | throw new Error("Query error: Can only parse strings, got {$valueNode->kind}", $valueNode); 33 | } 34 | 35 | return $this->tryParsingDate($valueNode->value, Error::class); 36 | } 37 | 38 | /** 39 | * @template T of Error|InvariantViolation 40 | * 41 | * @param class-string $exceptionClass 42 | * 43 | * @throws T 44 | */ 45 | protected function tryParsingDate(mixed $value, string $exceptionClass): \DateTimeInterface 46 | { 47 | if (\is_string($value)) { 48 | if (preg_match(static::regex(), $value, $matches) !== 1) { 49 | $regex = static::regex(); 50 | throw new $exceptionClass("Value \"{$value}\" does not match \"{$regex}\". Make sure it's ISO 8601 compliant "); 51 | } 52 | 53 | if (! $this->validateDate($matches['date'])) { 54 | $safeValue = Utils::printSafeJson($value); 55 | throw new $exceptionClass("Given input value is not ISO 8601 compliant: {$safeValue}."); 56 | } 57 | 58 | try { 59 | return new \DateTimeImmutable($value); 60 | } catch (\Exception $e) { 61 | throw new $exceptionClass($e->getMessage()); 62 | } 63 | } 64 | 65 | $safeValue = Utils::printSafeJson($value); 66 | throw new $exceptionClass("Cannot parse non-string into date: {$safeValue}"); 67 | } 68 | 69 | abstract protected static function outputFormat(): string; 70 | 71 | abstract protected static function regex(): string; 72 | 73 | private function validateDate(string $date): bool 74 | { 75 | // Verify the correct number of days for the month contained in the date-string. 76 | $year = (int) substr($date, 0, 4); 77 | $month = (int) substr($date, 5, 2); 78 | $day = (int) substr($date, 8, 2); 79 | 80 | switch ($month) { 81 | case 2: // February 82 | $isLeapYear = $this->isLeapYear($year); 83 | if ($isLeapYear && $day > 29) { 84 | return false; 85 | } 86 | 87 | return $isLeapYear || $day <= 28; 88 | 89 | case 4: // April 90 | case 6: // June 91 | case 9: // September 92 | case 11: // November 93 | if ($day > 30) { 94 | return false; 95 | } 96 | 97 | break; 98 | } 99 | 100 | return true; 101 | } 102 | 103 | private function isLeapYear(int $year): bool 104 | { 105 | return ($year % 4 === 0 && $year % 100 !== 0) 106 | || $year % 400 === 0; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/DateTime.php: -------------------------------------------------------------------------------- 1 | \d{4}-(0[1-9]|1[012])-(0[1-9]|[12][\d]|3[01])) ([01][\d]|2[0-3]):([0-5][\d]):([0-5][\d]|60)$~'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/DateTimeTz.php: -------------------------------------------------------------------------------- 1 | \d{4}-(0[1-9]|1[012])-(0[1-9]|[12][\d]|3[01]))T([01][\d]|2[0-3]):([0-5][\d]):([0-5][\d]|60))(\.\d+)?(([Z])|([+|-]([01][\d]|2[0-3]):[0-5][\d]))$~'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Email.php: -------------------------------------------------------------------------------- 1 | isValid( 16 | $stringValue, 17 | new RFCValidation() 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/IntRange.php: -------------------------------------------------------------------------------- 1 | isValueInExpectedRange($value)) { 23 | return $value; 24 | } 25 | 26 | $notInRange = Utils::printSafe($value); 27 | throw new \InvalidArgumentException("Value not in range {$this->rangeDescription()}: {$notInRange}."); 28 | } 29 | 30 | public function parseValue($value) 31 | { 32 | if (is_int($value) && $this->isValueInExpectedRange($value)) { 33 | return $value; 34 | } 35 | 36 | $notInRange = Utils::printSafe($value); 37 | throw new Error("Value not in range {$this->rangeDescription()}: {$notInRange}."); 38 | } 39 | 40 | public function parseLiteral(Node $valueNode, ?array $variables = null) 41 | { 42 | if ($valueNode instanceof IntValueNode) { 43 | $value = (int) $valueNode->value; 44 | if ($this->isValueInExpectedRange($value)) { 45 | return $value; 46 | } 47 | } 48 | 49 | $notInRange = Printer::doPrint($valueNode); 50 | throw new Error("Value not in range {$this->rangeDescription()}: {$notInRange}.", $valueNode); 51 | } 52 | 53 | private function isValueInExpectedRange(int $value): bool 54 | { 55 | return $value <= static::max() && $value >= static::min(); 56 | } 57 | 58 | private function rangeDescription(): string 59 | { 60 | $min = static::min(); 61 | $max = static::max(); 62 | 63 | return "{$min}-{$max}"; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/JSON.php: -------------------------------------------------------------------------------- 1 | decodeJSON($value); 23 | } 24 | 25 | public function parseLiteral($valueNode, ?array $variables = null) 26 | { 27 | if (! property_exists($valueNode, 'value')) { 28 | $withoutValue = Printer::doPrint($valueNode); 29 | throw new Error("Can not parse literals without a value: {$withoutValue}."); 30 | } 31 | 32 | return $this->decodeJSON($valueNode->value); 33 | } 34 | 35 | /** 36 | * Try to decode a user-given JSON value. 37 | * 38 | * @param mixed $value A user given JSON 39 | * 40 | * @throws Error 41 | * 42 | * @return mixed The decoded value 43 | */ 44 | protected function decodeJSON(mixed $value): mixed 45 | { 46 | try { 47 | // @phpstan-ignore-next-line we attempt unsafe values and let it throw 48 | $decoded = \Safe\json_decode($value); 49 | } catch (JsonException $jsonException) { 50 | throw new Error($jsonException->getMessage()); 51 | } 52 | 53 | return $decoded; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/MixedScalar.php: -------------------------------------------------------------------------------- 1 | name = $name; 37 | $concreteRegex->description = $description; 38 | $concreteRegex::$regex = $regex; 39 | 40 | return $concreteRegex; 41 | } 42 | 43 | /** Return the Regex that the values are validated against. */ 44 | abstract public static function regex(): string; 45 | 46 | public function serialize($value): string 47 | { 48 | $stringValue = Utils::coerceToString($value, InvariantViolation::class); 49 | 50 | if (! static::matchesRegex($stringValue)) { 51 | throw new InvariantViolation(static::unmatchedRegexMessage($stringValue)); 52 | } 53 | 54 | return $stringValue; 55 | } 56 | 57 | /** Determine if the given string matches the regex defined in this class. */ 58 | protected static function matchesRegex(string $value): bool 59 | { 60 | return RegexValidator::match(static::regex(), $value) 61 | ->hasMatch(); 62 | } 63 | 64 | public function parseValue($value): string 65 | { 66 | $stringValue = Utils::coerceToString($value, Error::class); 67 | 68 | if (! static::matchesRegex($stringValue)) { 69 | throw new Error(static::unmatchedRegexMessage($stringValue)); 70 | } 71 | 72 | return $stringValue; 73 | } 74 | 75 | public function parseLiteral($valueNode, ?array $variables = null): string 76 | { 77 | $value = Utils::extractStringFromLiteral($valueNode); 78 | 79 | if (! static::matchesRegex($value)) { 80 | throw new Error(static::unmatchedRegexMessage($value), $valueNode); 81 | } 82 | 83 | return $value; 84 | } 85 | 86 | /** Construct the error message that occurs when the given string does not match the regex. */ 87 | public static function unmatchedRegexMessage(string $value): string 88 | { 89 | $safeValue = GraphQLUtils::printSafeJson($value); 90 | $regex = static::regex(); 91 | 92 | return "The given value {$safeValue} did not match the regex {$regex}."; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/StringScalar.php: -------------------------------------------------------------------------------- 1 | isValid)($stringValue); 28 | } 29 | }; 30 | 31 | $concreteStringScalar->name = $name; 32 | $concreteStringScalar->description = $description; 33 | $concreteStringScalar->isValid = $isValid; 34 | 35 | return $concreteStringScalar; 36 | } 37 | 38 | /** Check if the given string is valid. */ 39 | abstract protected function isValid(string $stringValue): bool; 40 | 41 | public function serialize($value): string 42 | { 43 | $stringValue = Utils::coerceToString($value, InvariantViolation::class); 44 | 45 | if (! $this->isValid($stringValue)) { 46 | throw new InvariantViolation($this->invalidStringMessage($stringValue)); 47 | } 48 | 49 | return $stringValue; 50 | } 51 | 52 | /** Construct an error message that occurs when an invalid string is passed. */ 53 | public function invalidStringMessage(string $stringValue): string 54 | { 55 | $safeValue = GraphQLUtils::printSafeJson($stringValue); 56 | 57 | return "The given string {$safeValue} is not a valid {$this->inferName()}."; 58 | } 59 | 60 | public function parseValue($value): string 61 | { 62 | $stringValue = Utils::coerceToString($value, Error::class); 63 | 64 | if (! $this->isValid($stringValue)) { 65 | throw new Error($this->invalidStringMessage($stringValue)); 66 | } 67 | 68 | return $stringValue; 69 | } 70 | 71 | public function parseLiteral($valueNode, ?array $variables = null): string 72 | { 73 | $stringValue = Utils::extractStringFromLiteral($valueNode); 74 | 75 | if (! $this->isValid($stringValue)) { 76 | throw new Error($this->invalidStringMessage($stringValue), $valueNode); 77 | } 78 | 79 | return $stringValue; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Utils.php: -------------------------------------------------------------------------------- 1 | kind}", $valueNode); 32 | } 33 | 34 | return $valueNode->value; 35 | } 36 | 37 | /** 38 | * Convert the value to a string or throw. 39 | * 40 | * @template T of \Throwable 41 | * 42 | * @param class-string $exceptionClass 43 | * 44 | * @throws T 45 | */ 46 | public static function coerceToString(mixed $value, string $exceptionClass): string 47 | { 48 | if (! self::canBeString($value)) { 49 | $safeValue = GraphQLUtils::printSafeJson($value); 50 | throw new $exceptionClass("The given value can not be coerced to a string: {$safeValue}."); 51 | } 52 | 53 | // @phpstan-ignore-next-line we have proven the value can be safely cast 54 | return (string) $value; 55 | } 56 | } 57 | --------------------------------------------------------------------------------