Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
34ef7b5
Allow setting per-schema scalar overrides explicitly via SchemaConfig
tmoitie Jun 11, 2026
a399008
Merge branch 'master' into fix-lazy-types-scalar-override-scan
tmoitie Jun 17, 2026
6a4e016
Update dependency phpstan/phpstan-phpunit to v2.0.18 (#1942)
renovate[bot] Jul 4, 2026
8fb0c13
Update dependency phpstan/phpstan to v2.2.5 (#1943)
renovate[bot] Jul 5, 2026
e7ee6ad
feat: Make promises generic over adopted type
simPod Jul 5, 2026
c72fd0f
v15.34.0 changelog
spawnia Jul 5, 2026
26035d4
Update dependency friendsofphp/php-cs-fixer to v3.95.12 (#1944)
renovate[bot] Jul 7, 2026
8f81f4b
fix(utils): preserve deprecated input values in client schema
simPod Jul 9, 2026
2e1f1f6
test(utils): avoid redundant client schema print coverage
simPod Jul 9, 2026
e633f9e
Merge branch 'master' into fix-lazy-types-scalar-override-scan
tmoitie Jul 9, 2026
ad29578
Update dependency friendsofphp/php-cs-fixer to v3.95.13 (#1946)
renovate[bot] Jul 10, 2026
8363b2a
Update dependency friendsofphp/php-cs-fixer to v3.95.15 (#1947)
renovate[bot] Jul 15, 2026
9b3eab7
Optimize Lexer::readString and Visitor::visitInParallel
OpaqueRock Jul 16, 2026
28bea9a
Update dependency phpstan/phpstan-strict-rules to v2.0.12 (#1950)
renovate[bot] Jul 19, 2026
28aa0bc
npx --yes prettier
spawnia Jul 19, 2026
5e99c3e
Apply latest rector fixes to tests
spawnia Jul 19, 2026
3c9d876
Skip overly eager RemoveDuplicatedReturnSelfDocblockRector
spawnia Jul 19, 2026
203d11e
Remove skips for deprecated Rector rules
spawnia Jul 19, 2026
fcd07c3
Merge remote-tracking branch 'origin/master' into fix/client-schema-d…
spawnia Jul 20, 2026
19ff1e0
Merge branch 'master' into fix-lazy-types-scalar-override-scan
spawnia Jul 20, 2026
dee2bd7
Autofix
autofix-ci[bot] Jul 20, 2026
ef0969c
Merge remote-tracking branch 'origin/master' into pr-1948-OpaqueRock
spawnia Jul 20, 2026
239febc
Add test covering setScalarOverrides(null) reset branch
tmoitie Jul 20, 2026
0d3f87c
Merge pull request #1945 from simPod/fix/client-schema-deprecated-inp…
spawnia Jul 21, 2026
9e2cdbc
Update CHANGELOG for v15.34.1
spawnia Jul 21, 2026
a84439a
Merge branch 'master' into fix-lazy-types-scalar-override-scan
tmoitie Jul 21, 2026
5605b5b
Fix visitInParallel() dropping visitors for custom Node kinds
OpaqueRock Jul 22, 2026
44f76e8
Address review feedback on Lexer/Visitor optimization
OpaqueRock Jul 22, 2026
cf27f7e
Remove visit() custom-kind bugfix, split into #1952
OpaqueRock Jul 22, 2026
caa28dd
Point scalar override comment at motivating lighthouse issue
tmoitie Jul 22, 2026
9b0d5cc
Move scalar override validation into schema validation
tmoitie Jul 22, 2026
31d6d9e
Autofix
autofix-ci[bot] Jul 22, 2026
65089c9
Remove Visitor optimization, split into #1953
OpaqueRock Jul 22, 2026
70bd47c
Merge pull request #1927 from deskpro/fix-lazy-types-scalar-override-…
spawnia Jul 23, 2026
b67de08
Update CHANGELOG for v15.35.0
spawnia Jul 23, 2026
f9e81d7
Clarify release notes format in AGENTS.md
spawnia Jul 23, 2026
031cb52
Fix loose switch comparison mistaking NUL byte for EOF in string escapes
OpaqueRock Jul 24, 2026
240f5b9
Update dependency friendsofphp/php-cs-fixer to v3.95.17 (#1955)
renovate[bot] Jul 24, 2026
cc722fd
Use mb_strlen for codepoint counting, simplify tab-character test
OpaqueRock Jul 25, 2026
79f8304
perf(amp): avoid fibers in future adapter
simPod Jul 25, 2026
ae93bd6
Update dependency phpstan/phpstan to v2.2.6 (#1956)
renovate[bot] Jul 26, 2026
b5b209b
.gitattributes: Add .worktreeinclude
reedy Jul 27, 2026
ab0ccf8
Add tests for full Lexer.php coverage
spawnia Jul 28, 2026
47ded87
Merge pull request #1948 from OpaqueRock/optimize-input-processing
spawnia Jul 28, 2026
be55406
Release v15.36.0
spawnia Jul 28, 2026
8b8eaad
Merge branch 'master' into pr-1954-simPod
spawnia Jul 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .ai/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ After merging a PR and releasing:
2. Pull latest master: `git pull`
3. Update CHANGELOG.md: move the entry from `## Unreleased` into a new versioned section (e.g. `## v15.32.0`), add the PR URL as a reference, leave an empty `## Unreleased` at the top
4. Commit and push the CHANGELOG update to master
5. Create the release: `gh release create vX.Y.Z --repo webonyx/graphql-php --title "vX.Y.Z" --notes "..."`
6. Comment on the PR thanking the author and linking the release: `gh pr comment <number> --repo webonyx/graphql-php --body "Thanks @<author>! Released as [vX.Y.Z](<url>)."`
5. Create the release: `gh release create vX.Y.Z --repo=webonyx/graphql-php --title="vX.Y.Z" --notes="<content of the new CHANGELOG section>"`
6. Comment on the PR thanking the author and linking the release: `gh pr comment <number> --repo=webonyx/graphql-php --body="Thanks @<author>! Released as [vX.Y.Z](<url>)."`

Version bump rules (semver):
- New feature → minor bump (v15.31.x → v15.32.0)
Expand Down
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@
/rector.php export-ignore
/renovate.json export-ignore
/UPGRADE.md export-ignore
/.worktreeinclude export-ignore
2 changes: 1 addition & 1 deletion .github/workflows/autofix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:

- run: composer docs

- run: npx prettier --write --tab-width=2 '*.md' '**/*.md'
- run: npx --yes prettier --write --tab-width=2 '*.md' '**/*.md'

- uses: autofix-ci/action@v1
with:
Expand Down
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,34 @@ You can find and compare releases at the [GitHub release page](https://github.co

## Unreleased

### Fixed

- Avoid creating an AMPHP Fiber for each `AmpFutureAdapter` continuation and aggregate https://github.com/webonyx/graphql-php/pull/1954

## v15.36.0

### Changed

- Optimize `Lexer::readString()` to bulk-scan ordinary bytes with `strcspn()` instead of decoding one UTF-8 character at a time https://github.com/webonyx/graphql-php/pull/1948

## v15.35.0

### Added

- Allow setting per-schema built-in scalar overrides explicitly via `SchemaConfig` option `scalarOverrides`, avoiding the resolution of lazily provided `types` on built-in scalar lookups https://github.com/webonyx/graphql-php/pull/1927

## v15.34.1

### Fixed

- Preserve `deprecationReason` when rebuilding input values from introspection in `BuildClientSchema` https://github.com/webonyx/graphql-php/pull/1945

## v15.34.0

### Added

- Make Promise and PromiseAdapter generic over the adopted promise type https://github.com/webonyx/graphql-php/pull/1941

## v15.33.1

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ php-cs-fixer: ## Fix code style

.PHONY: prettier
prettier: ## Format code with prettier
npx prettier --write --tab-width=2 '*.md' '**/*.md'
npx --yes prettier --write --tab-width=2 '*.md' '**/*.md'

phpstan.neon:
printf "includes:\n - phpstan.neon.dist" > phpstan.neon
Expand All @@ -52,7 +52,7 @@ bench: ## Runs benchmarks with PHPBench
.PHONY: docs
docs: ## Generate the class-reference docs
php generate-class-reference.php
npx prettier --write docs/class-reference.md
npx --yes prettier --write docs/class-reference.md

.PHONY: ai-sync
ai-sync: ## Generate local agent configuration from .ai
Expand Down
8 changes: 4 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@
"amphp/http-server": "^2.1 || ^3",
"dms/phpunit-arraysubset-asserts": "dev-master",
"ergebnis/composer-normalize": "^2.28",
"friendsofphp/php-cs-fixer": "3.95.11",
"friendsofphp/php-cs-fixer": "3.95.17",
"mll-lab/php-cs-fixer-config": "5.13.0",
"nyholm/psr7": "^1.5",
"phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "2.2.4",
"phpstan/phpstan-phpunit": "2.0.17",
"phpstan/phpstan-strict-rules": "2.0.11",
"phpstan/phpstan": "2.2.6",
"phpstan/phpstan-phpunit": "2.0.18",
"phpstan/phpstan-strict-rules": "2.0.12",
"phpunit/phpunit": "^9.5 || ^10.5.21 || ^11",
"psr/http-message": "^1 || ^2",
"react/http": "^1.6",
Expand Down
40 changes: 38 additions & 2 deletions docs/class-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,7 @@ Usage example:
mutation?: MaybeLazyObjectType,
subscription?: MaybeLazyObjectType,
types?: Types|null,
scalarOverrides?: array<ScalarType>|null,
directives?: array<Directive>|null,
typeLoader?: TypeLoader|null,
assumeValid?: bool|null,
Expand Down Expand Up @@ -911,6 +912,27 @@ function getTypes()
function setTypes($types): self
```

```php
/**
* @return array<string, ScalarType>|null
*
* @api
*/
function getScalarOverrides(): ?array
```

```php
/**
* Deeper validation (that each override is a ScalarType named after a built-in scalar)
* runs during schema validation, see SchemaValidationContext::validateScalarOverrides().
*
* @param array<ScalarType>|null $scalarOverrides
*
* @api
*/
function setScalarOverrides(?array $scalarOverrides): self
```

```php
/**
* @return array<Directive>|null
Expand Down Expand Up @@ -1805,6 +1827,8 @@ function toArray(int $debug = 'GraphQL\\Error\\DebugFlag::NONE'): array

Provides a means for integration of async PHP platforms ([related docs](data-fetching.md#async-php)).

@template TAdopted = mixed

### GraphQL\Executor\Promise\PromiseAdapter Methods

```php
Expand All @@ -1824,6 +1848,8 @@ function isThenable($value): bool
*
* @param mixed $thenable
*
* @phpstan-return Promise<TAdopted>
*
* @api
*/
function convertThenable($thenable): GraphQL\Executor\Promise\Promise
Expand All @@ -1834,6 +1860,10 @@ function convertThenable($thenable): GraphQL\Executor\Promise\Promise
* Accepts our Promise wrapper, extracts adopted promise out of it and executes actual `then` logic described
* in Promises/A+ specs. Then returns new wrapped instance of GraphQL\Executor\Promise\Promise.
*
* @phpstan-param Promise<covariant TAdopted> $promise
*
* @phpstan-return Promise<TAdopted>
*
* @api
*/
function then(
Expand All @@ -1849,6 +1879,8 @@ function then(
*
* @param callable(callable $resolve, callable $reject): void $resolver
*
* @phpstan-return Promise<TAdopted>
*
* @api
*/
function create(callable $resolver): GraphQL\Executor\Promise\Promise
Expand All @@ -1860,16 +1892,18 @@ function create(callable $resolver): GraphQL\Executor\Promise\Promise
*
* @param mixed $value
*
* @phpstan-return Promise<TAdopted>
*
* @api
*/
function createFulfilled($value = null): GraphQL\Executor\Promise\Promise
```

```php
/**
* Creates a rejected promise for a reason if the reason is not a promise.
* Return a promise rejected with the given reason.
*
* If the provided reason is a promise, then it is returned as-is.
* @phpstan-return Promise<TAdopted>
*
* @api
*/
Expand All @@ -1883,6 +1917,8 @@ function createRejected(Throwable $reason): GraphQL\Executor\Promise\Promise
*
* @param iterable<Promise|mixed> $promisesOrValues
*
* @phpstan-return Promise<TAdopted>
*
* @api
*/
function all(iterable $promisesOrValues): GraphQL\Executor\Promise\Promise
Expand Down
17 changes: 9 additions & 8 deletions docs/schema-definition.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,15 @@ with complex input values (see [Mutations and Input Types](type-definitions/inpu
The schema constructor expects an instance of [`GraphQL\Type\SchemaConfig`](class-reference.md#graphqltypeschemaconfig)
or an array with the following options:

| Option | Type | Notes |
| ------------ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| query | `ObjectType` or `callable(): ?ObjectType` or `null` | **Required.** Object type (usually named `Query`) containing root-level fields of your read API |
| mutation | `ObjectType` or `callable(): ?ObjectType` or `null` | Object type (usually named `Mutation`) containing root-level fields of your write API |
| subscription | `ObjectType` or `callable(): ?ObjectType` or `null` | Reserved for future subscriptions implementation. Currently presented for compatibility with introspection query of **graphql-js**, used by various clients (like Relay or GraphiQL) |
| directives | `array<Directive>` | A full list of [directives](type-definitions/directives.md) supported by your schema. By default, contains built-in **@skip** and **@include** directives.<br><br> If you pass your own directives and still want to use built-in directives - add them explicitly. For example:<br><br> _array_merge(GraphQL::getStandardDirectives(), [$myCustomDirective]);_ |
| types | `iterable<Type&NamedType>` | Additional types to register in the schema.<br><br>Use this for object types that are not directly referenced in fields but implement an interface that resolves to them via **resolveType**.<br><br>Can also contain custom scalar types named like built-in scalars (`String`, `Int`, etc.) to [override them](type-definitions/scalars.md#overriding-built-in-scalars) on a per-schema basis. |
| typeLoader | `callable(string $name): Type` | Expected to return a type instance given the name. Must always return the same instance if called multiple times, see [lazy loading](#lazy-loading-of-types). See section below on lazy type loading. |
| Option | Type | Notes |
| --------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| query | `ObjectType` or `callable(): ?ObjectType` or `null` | **Required.** Object type (usually named `Query`) containing root-level fields of your read API |
| mutation | `ObjectType` or `callable(): ?ObjectType` or `null` | Object type (usually named `Mutation`) containing root-level fields of your write API |
| subscription | `ObjectType` or `callable(): ?ObjectType` or `null` | Reserved for future subscriptions implementation. Currently presented for compatibility with introspection query of **graphql-js**, used by various clients (like Relay or GraphiQL) |
| directives | `array<Directive>` | A full list of [directives](type-definitions/directives.md) supported by your schema. By default, contains built-in **@skip** and **@include** directives.<br><br> If you pass your own directives and still want to use built-in directives - add them explicitly. For example:<br><br> _array_merge(GraphQL::getStandardDirectives(), [$myCustomDirective]);_ |
| types | `iterable<Type&NamedType>` | Additional types to register in the schema.<br><br>Use this for object types that are not directly referenced in fields but implement an interface that resolves to them via **resolveType**.<br><br>Can also contain custom scalar types named like built-in scalars (`String`, `Int`, etc.) to [override them](type-definitions/scalars.md#overriding-built-in-scalars) on a per-schema basis. |
| scalarOverrides | `array<ScalarType>` or `null` | Custom scalar types named like built-in scalars (`String`, `Int`, etc.) that [override them](type-definitions/scalars.md#overriding-built-in-scalars) on a per-schema basis.<br><br>When `null` (default), overrides are discovered by scanning **types**, which fully resolves it even when given as a lazy callable. Pass overrides explicitly (or an empty array for none) to skip the scan. |
| typeLoader | `callable(string $name): Type` | Expected to return a type instance given the name. Must always return the same instance if called multiple times, see [lazy loading](#lazy-loading-of-types). See section below on lazy type loading. |

### Using config class

Expand Down
15 changes: 14 additions & 1 deletion docs/type-definitions/scalars.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,17 @@ $schema = new Schema([
The custom scalar replaces the built-in one throughout the entire schema, affecting both serialization of results and coercion of inputs.

> **Note:** The `typeLoader` is never called for built-in scalar names.
> Always use `types` to override them.
> Always use `types` or `scalarOverrides` to override them.

Discovering overrides requires scanning `types`.
When `types` is a lazy callable, the scan resolves it on the first lookup of a built-in scalar - typically the first executed query.
To avoid this in schemas that use [lazy loading of types](../schema-definition.md#lazy-loading-of-types), pass overrides explicitly through the `scalarOverrides` option instead - or pass an empty array to declare there are none:

```php
$schema = new Schema([
'query' => $queryType,
'typeLoader' => $myTypeLoader,
'types' => $myLazyTypes,
'scalarOverrides' => [$uppercaseString], // or [] for none
]);
```
16 changes: 16 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ parameters:
- src/Executor/Promise
- examples/04-async-php

# The v2 AmpPromiseAdapter wraps concrete Amp\Success/Amp\Failure; against the
# invariant Amp\Promise<mixed> binding PHPStan reports a return mismatch (as
# Amp\Success<mixed> on a v2 install, or bare Amp\Success when v2 is absent on a
# v3 install). Tolerate either form / either state.
- message: "~Method GraphQL\\\\Executor\\\\Promise\\\\Adapter\\\\AmpPromiseAdapter::create(Fulfilled|Rejected)\\(\\) should return GraphQL\\\\Executor\\\\Promise\\\\Promise<Amp\\\\Promise<mixed>> but returns GraphQL\\\\Executor\\\\Promise\\\\Promise<(Amp\\\\Promise<mixed>\\|)?Amp\\\\(Success|Failure)(<mixed>)?(\\|Amp\\\\Promise<mixed>)?>~"
reportUnmatched: false
path: src/Executor/Promise/Adapter/AmpPromiseAdapter.php

# AmpFutureAdapter::createFulfilled()/createRejected() build a concrete Amp\Future
# whose generic arg PHPStan infers narrower than the invariant Amp\Future<mixed>
# binding. Only reported when amphp/amp v3 is installed (absent under v2), so
# tolerate either state.
- message: "~Method GraphQL\\\\Executor\\\\Promise\\\\Adapter\\\\AmpFutureAdapter::create(Fulfilled|Rejected)\\(\\) should return GraphQL\\\\Executor\\\\Promise\\\\Promise<Amp\\\\Future<mixed>> but returns GraphQL\\\\Executor\\\\Promise\\\\Promise<Amp\\\\Future(<[a-z]+>)?>~"
reportUnmatched: false
path: src/Executor/Promise/Adapter/AmpFutureAdapter.php

includes:
- phpstan-baseline.neon
- phpstan/include-by-php-version.php
Expand Down
5 changes: 1 addition & 4 deletions rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,8 @@
$rectorConfig->skip([
Rector\CodeQuality\Rector\Isset_\IssetOnPropertyObjectToPropertyExistsRector::class, // isset() is nice when moving towards typed properties
Rector\CodeQuality\Rector\Identical\FlipTypeControlToUseExclusiveTypeRector::class, // Unnecessarily complex with PHPStan
Rector\CodeQuality\Rector\Concat\JoinStringConcatRector::class => [
__DIR__ . '/tests', // Sometimes more readable for long strings
],
Rector\CodeQuality\Rector\ClassMethod\LocallyCalledStaticMethodToNonStaticRector::class, // static methods are fine
Rector\CodeQuality\Rector\Foreach_\UnusedForeachValueToArrayKeysRector::class, // Less efficient
Rector\CodeQuality\Rector\Switch_\SwitchTrueToIfRector::class, // More expressive in some cases
Rector\DeadCode\Rector\If_\RemoveAlwaysTrueIfConditionRector::class, // Sometimes necessary to prove runtime behavior matches defined types
Rector\DeadCode\Rector\If_\RemoveDeadInstanceOfRector::class, // Sometimes necessary to prove runtime behavior matches defined types
Rector\DeadCode\Rector\Node\RemoveNonExistingVarAnnotationRector::class, // Sometimes false-positive
Expand All @@ -38,6 +34,7 @@
__DIR__ . '/tests/TestCaseBase.php', // Array output may differ between tested PHP versions, assertEquals smooths over this
],
Rector\PHPUnit\PHPUnit60\Rector\ClassMethod\AddDoesNotPerformAssertionToNonAssertingTestRector::class, // False-positive
Rector\DeadCode\Rector\ClassMethod\RemoveDuplicatedReturnSelfDocblockRector::class, // Overly eager on removing static or $this
]);
$rectorConfig->paths([
__DIR__ . '/benchmarks',
Expand Down
Loading
Loading