Skip to content

Commit 243d6c2

Browse files
committed
Introduce scope-aware Context and #[MapContext]
Resolvers often need per-request data that is neither a GraphQL input nor a DI service: the current HTTP request, the authenticated user, or a value computed by an ancestor field. Until now the generated resolver closures ignored webonyx's context value entirely, so the only escape hatches were `#[Arg]` (client input) and `#[Autowire]` (container), and there was no way to thread request state to a resolver. This adds a first-class `Context` value object and a `#[MapContext]` parameter attribute. `Context` is a webonyx `ScopedContext` that stores objects by class (`attachObject`/`getObject`/`maybeGetObject`) and also implements `ArrayAccess`; because it is scoped, a value attached by a resolver is visible to its descendants without leaking to siblings. Resolvers reach it in two ways. A parameter type-hinted `Context` (no attribute) receives the whole context object, recognised automatically by type, so a resolver can read several values or `attachObject()` something for its descendant fields. `#[MapContext]` instead injects a single object from the `Context` by the parameter type — nullable parameters resolve to `null` when absent, non-nullable parameters throw. The generated query, mutation, and field closures now receive webonyx's context as a `mixed` value and pass it through to `ArgumentNodeResolver`. The value is left untouched (it can be anything the server provides); only `Context`/`#[MapContext]` resolution requires it to be a `Context`, throwing a `ContextException` otherwise. Wiring a `Context` onto the server is up to the consumer via the webonyx `context` option (see the docs).
1 parent 4ddba02 commit 243d6c2

36 files changed

Lines changed: 1125 additions & 16 deletions

docs/getting_started.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ $server = new StandardServer(ServerConfig::create([
4545
]));
4646
```
4747

48+
💡 *Want to pass per-request data (the HTTP request, the current user, …) to your resolvers? Set a*
49+
`Context` *as the server's context value and read it with* `#[MapContext]`*. See*
50+
[#[MapContext]](usage.md#mapcontext) *and* [Context](usage.md#context).
51+
4852
📌 *This library does not create a GraphQL server for you.*
4953

5054
To learn how to set up a server, check the [webonyx documentation](https://webonyx.github.io/graphql-php/executing-queries/#using-server).

docs/usage.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ You can use the following attributes:
2121
- [#[Field]](#field)
2222
- [#[Arg]](#arg)
2323
- [#[Autowire]](#autowire)
24+
- [#[MapContext]](#mapcontext)
2425
- [#[Scalar]](#scalar)
2526
- [#[Cursor]](#cursor)
2627

@@ -455,6 +456,10 @@ This can make injecting services into a `#[Type]` challenging.
455456
That's where `#[Autowire]` comes in. You can use it inside `#[Type]` methods (marked with `#[Field]`) to automatically
456457
inject services via parameters.
457458

459+
It also works on `#[Query]` and `#[Mutation]` methods. Those resolvers are themselves resolved from the DI container,
460+
so they can usually receive dependencies through their constructor — but `#[Autowire]` is available there too when you
461+
prefer to inject a service per method.
462+
458463
```php
459464
use Jerowork\GraphqlAttributeSchema\Attribute\Autowire;
460465
use Jerowork\GraphqlAttributeSchema\Attribute\Type;
@@ -492,6 +497,132 @@ By default, the service to inject is determined by the parameter type. If needed
492497
|-----------|------------------------------------------------------------------------------------|
493498
| `service` | *(Optional)* Custom service identifier to retrieve from the DI container (PSR-11). |
494499

500+
### #[MapContext]
501+
502+
Webonyx/graphql-php passes a **context value** to every resolver. This library wraps it in a
503+
`Context` object (see [Context](#context)) that you can populate per request — for example with the
504+
current HTTP request, the authenticated user, or any value resolved higher up in the query tree.
505+
506+
`#[MapContext]` injects an object stored in that `Context` into a resolver parameter, looked up by the
507+
parameter's type. It works on `#[Query]`, `#[Mutation]`, and `#[Field]` method parameters.
508+
509+
```php
510+
use Jerowork\GraphqlAttributeSchema\Attribute\MapContext;
511+
use Jerowork\GraphqlAttributeSchema\Attribute\Query;
512+
use Symfony\Component\HttpFoundation\Request;
513+
514+
final readonly class YourQuery
515+
{
516+
#[Query]
517+
public function yourQuery(
518+
int $filter,
519+
#[MapContext]
520+
Request $request,
521+
): YourType {
522+
// $request is fetched from the Context by its class
523+
}
524+
}
525+
```
526+
527+
A resolver can also attach objects to the `Context` so descendant fields can read them through
528+
`#[MapContext]`. Because `Context` is a webonyx `ScopedContext`, children receive a clone, so values
529+
attached by a resolver are visible to its descendants without leaking to sibling fields:
530+
531+
```php
532+
#[Field]
533+
public function getChild(Context $context): ChildType
534+
{
535+
$context->attachObject(new Locale('en'));
536+
// ... children resolving under this field can now `#[MapContext] Locale $locale`
537+
}
538+
```
539+
540+
#### Nullable parameters
541+
542+
- A **non-nullable** parameter (`Request $request`) throws a `ContextException` when no matching object
543+
is present in the `Context`.
544+
- A **nullable** parameter (`?Request $request`) resolves to `null` when no matching object is present.
545+
546+
#### Requirements
547+
548+
- The parameter must be type-hinted with a class (not a built-in type).
549+
- The GraphQL context value **must** be an instance of
550+
`Jerowork\GraphqlAttributeSchema\Context`. If it is anything else, a `ContextException` is thrown when
551+
the parameter is resolved. See [Context](#context) for how to set this up on your server.
552+
553+
### Context
554+
555+
`Jerowork\GraphqlAttributeSchema\Context` is the per-request context value passed to your resolvers. It
556+
stores objects keyed by their class and also implements `ArrayAccess`, so you can use it as a typed
557+
container or as a simple map.
558+
559+
There are two ways to read it from a resolver, and they do different things:
560+
561+
- **Type-hint `Context $context`** (no attribute) — the resolver receives the **whole `Context` object**.
562+
This is built into the framework: any parameter typed `Context` is recognised automatically. Use it to
563+
read multiple values, or to `attachObject()` something for descendant fields.
564+
- **`#[MapContext] Request $request`** — the resolver receives **a single item out of the `Context`**,
565+
looked up by the parameter's type (here `Request`). See [#[MapContext]](#mapcontext).
566+
567+
```php
568+
use Jerowork\GraphqlAttributeSchema\Attribute\MapContext;
569+
use Jerowork\GraphqlAttributeSchema\Context;
570+
use Symfony\Component\HttpFoundation\Request;
571+
572+
#[Query]
573+
public function yourQuery(
574+
Context $context, // the whole Context object
575+
#[MapContext]
576+
Request $request, // just the Request stored in the Context
577+
): YourType {
578+
// ...
579+
}
580+
```
581+
582+
The `Context` object itself:
583+
584+
```php
585+
use Jerowork\GraphqlAttributeSchema\Context;
586+
587+
$context = new Context();
588+
589+
// Attach by class
590+
$context->attachObject($request); // stored under Request::class
591+
$request = $context->getObject(Request::class); // throws if missing/wrong type
592+
$request = $context->maybeGetObject(Request::class); // null if missing
593+
594+
// Or as an array (any key)
595+
$context[Request::class] = $request;
596+
$locale = $context['locale'] ?? null;
597+
```
598+
599+
Set it on your server through the webonyx `context` option. Using a callable means a fresh `Context`
600+
is built per request:
601+
602+
```php
603+
use GraphQL\Error\DebugFlag;
604+
use GraphQL\Server\ServerConfig;
605+
use GraphQL\Server\StandardServer;
606+
use Jerowork\GraphqlAttributeSchema\Context;
607+
use Symfony\Component\HttpFoundation\Request;
608+
609+
$config = [
610+
'schema' => $this->createSchema(),
611+
'context' => function () {
612+
$context = new Context();
613+
$context[Request::class] = $this->requestStack->getMainRequest();
614+
615+
return $context;
616+
},
617+
];
618+
619+
if ($this->environment !== EnvironmentName::PROD) {
620+
$config['debugFlag'] = DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE | DebugFlag::RETHROW_INTERNAL_EXCEPTIONS;
621+
}
622+
623+
return new StandardServer(ServerConfig::create($config));
624+
```
625+
495626
### #[Scalar]
496627

497628
Webonyx/graphql-php comes with four built-in scalar types:

phpstan-baseline.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
'path' => __DIR__ . '/src/Node/Child/ArgNode.php',
99
];
1010
$ignoreErrors[] = [
11-
'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\Child\\\\FieldNode\\:\\:toArray\\(\\) should return array\\{reference\\: array\\{type\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\TypeReference\\\\TypeReference\\>, payload\\: array\\<string, mixed\\>\\}, name\\: string, description\\: string\\|null, argumentNodes\\: list\\<array\\{node\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\Child\\\\ArgumentNode\\>, payload\\: array\\{reference\\: array\\{type\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\TypeReference\\\\TypeReference\\>, payload\\: array\\<string, mixed\\>\\}, name\\: string, description\\: string\\|null, propertyName\\: string\\}\\|array\\{service\\?\\: string, propertyName\\: string\\}\\}\\>, fieldType\\: string, methodName\\: string\\|null, propertyName\\: string\\|null, deprecationReason\\: string\\|null, \\.\\.\\.\\} but returns array\\{reference\\: array\\{type\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\TypeReference\\\\TypeReference\\>&literal\\-string, payload\\: array\\<string, mixed\\>\\}, name\\: string, description\\: string\\|null, argumentNodes\\: list\\<array\\{node\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\Child\\\\ArgumentNode\\>&literal\\-string, payload\\: array\\<string, mixed\\>\\}\\>, fieldType\\: \'method\'\\|\'property\', methodName\\: string\\|null, propertyName\\: string\\|null, deprecationReason\\: string\\|null, \\.\\.\\.\\}\\.$#',
11+
'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\Child\\\\FieldNode\\:\\:toArray\\(\\) should return array\\{reference\\: array\\{type\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\TypeReference\\\\TypeReference\\>, payload\\: array\\<string, mixed\\>\\}, name\\: string, description\\: string\\|null, argumentNodes\\: list\\<array\\{node\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\Child\\\\ArgumentNode\\>, payload\\: array\\{className\\: class\\-string, propertyName\\: string, nullable\\: bool\\}\\|array\\{reference\\: array\\{type\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\TypeReference\\\\TypeReference\\>, payload\\: array\\<string, mixed\\>\\}, name\\: string, description\\: string\\|null, propertyName\\: string\\}\\|array\\{service\\?\\: string, propertyName\\: string\\}\\}\\>, fieldType\\: string, methodName\\: string\\|null, propertyName\\: string\\|null, deprecationReason\\: string\\|null, \\.\\.\\.\\} but returns array\\{reference\\: array\\{type\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\TypeReference\\\\TypeReference\\>&literal\\-string, payload\\: array\\<string, mixed\\>\\}, name\\: string, description\\: string\\|null, argumentNodes\\: list\\<array\\{node\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\Child\\\\ArgumentNode\\>&literal\\-string, payload\\: array\\<string, mixed\\>\\}\\>, fieldType\\: \'method\'\\|\'property\', methodName\\: string\\|null, propertyName\\: string\\|null, deprecationReason\\: string\\|null, \\.\\.\\.\\}\\.$#',
1212
'identifier' => 'return.type',
1313
'count' => 1,
1414
'path' => __DIR__ . '/src/Node/Child/FieldNode.php',
@@ -20,7 +20,7 @@
2020
'path' => __DIR__ . '/src/Node/Child/FieldNode.php',
2121
];
2222
$ignoreErrors[] = [
23-
'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\MutationNode\\:\\:toArray\\(\\) should return array\\{className\\: class\\-string, name\\: string, description\\: string\\|null, argumentNodes\\: list\\<array\\{node\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\Child\\\\ArgumentNode\\>, payload\\: array\\{propertyName\\: string\\}\\|array\\{reference\\: array\\{type\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\TypeReference\\\\TypeReference\\>, payload\\: array\\<string, mixed\\>\\}, name\\: string, description\\: string\\|null, propertyName\\: string\\}\\}\\>, outputReference\\: array\\{type\\: class\\-string, payload\\: array\\<string, mixed\\>\\}, methodName\\: string, deprecationReason\\: string\\|null, deferredTypeLoader\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Type\\\\Loader\\\\DeferredTypeLoader\\>\\|null\\} but returns array\\{className\\: class\\-string, name\\: string, description\\: string\\|null, argumentNodes\\: list\\<array\\{node\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\Child\\\\ArgumentNode\\>&literal\\-string, payload\\: array\\<string, mixed\\>\\}\\>, outputReference\\: array\\{type\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\TypeReference\\\\TypeReference\\>&literal\\-string, payload\\: array\\<string, mixed\\>\\}, methodName\\: string, deprecationReason\\: string\\|null, deferredTypeLoader\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Type\\\\Loader\\\\DeferredTypeLoader\\>\\|null\\}\\.$#',
23+
'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\MutationNode\\:\\:toArray\\(\\) should return array\\{className\\: class\\-string, name\\: string, description\\: string\\|null, argumentNodes\\: list\\<array\\{node\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\Child\\\\ArgumentNode\\>, payload\\: array\\{className\\: class\\-string, propertyName\\: string, nullable\\: bool\\}\\|array\\{reference\\: array\\{type\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\TypeReference\\\\TypeReference\\>, payload\\: array\\<string, mixed\\>\\}, name\\: string, description\\: string\\|null, propertyName\\: string\\}\\|array\\{service\\?\\: string, propertyName\\: string\\}\\}\\>, outputReference\\: array\\{type\\: class\\-string, payload\\: array\\<string, mixed\\>\\}, methodName\\: string, deprecationReason\\: string\\|null, deferredTypeLoader\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Type\\\\Loader\\\\DeferredTypeLoader\\>\\|null\\} but returns array\\{className\\: class\\-string, name\\: string, description\\: string\\|null, argumentNodes\\: list\\<array\\{node\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\Child\\\\ArgumentNode\\>&literal\\-string, payload\\: array\\<string, mixed\\>\\}\\>, outputReference\\: array\\{type\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\TypeReference\\\\TypeReference\\>&literal\\-string, payload\\: array\\<string, mixed\\>\\}, methodName\\: string, deprecationReason\\: string\\|null, deferredTypeLoader\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Type\\\\Loader\\\\DeferredTypeLoader\\>\\|null\\}\\.$#',
2424
'identifier' => 'return.type',
2525
'count' => 1,
2626
'path' => __DIR__ . '/src/Node/MutationNode.php',
@@ -32,7 +32,7 @@
3232
'path' => __DIR__ . '/src/Node/MutationNode.php',
3333
];
3434
$ignoreErrors[] = [
35-
'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\QueryNode\\:\\:toArray\\(\\) should return array\\{className\\: class\\-string, name\\: string, description\\: string\\|null, argumentNodes\\: list\\<array\\{node\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\Child\\\\ArgumentNode\\>, payload\\: array\\{propertyName\\: string\\}\\|array\\{reference\\: array\\{type\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\TypeReference\\\\TypeReference\\>, payload\\: array\\<string, mixed\\>\\}, name\\: string, description\\: string\\|null, propertyName\\: string\\}\\}\\>, outputReference\\: array\\{type\\: class\\-string, payload\\: array\\<string, mixed\\>\\}, methodName\\: string, deprecationReason\\: string\\|null, deferredTypeLoader\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Type\\\\Loader\\\\DeferredTypeLoader\\>\\|null\\} but returns array\\{className\\: class\\-string, name\\: string, description\\: string\\|null, argumentNodes\\: list\\<array\\{node\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\Child\\\\ArgumentNode\\>&literal\\-string, payload\\: array\\<string, mixed\\>\\}\\>, outputReference\\: array\\{type\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\TypeReference\\\\TypeReference\\>&literal\\-string, payload\\: array\\<string, mixed\\>\\}, methodName\\: string, deprecationReason\\: string\\|null, deferredTypeLoader\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Type\\\\Loader\\\\DeferredTypeLoader\\>\\|null\\}\\.$#',
35+
'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\QueryNode\\:\\:toArray\\(\\) should return array\\{className\\: class\\-string, name\\: string, description\\: string\\|null, argumentNodes\\: list\\<array\\{node\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\Child\\\\ArgumentNode\\>, payload\\: array\\{className\\: class\\-string, propertyName\\: string, nullable\\: bool\\}\\|array\\{reference\\: array\\{type\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\TypeReference\\\\TypeReference\\>, payload\\: array\\<string, mixed\\>\\}, name\\: string, description\\: string\\|null, propertyName\\: string\\}\\|array\\{service\\?\\: string, propertyName\\: string\\}\\}\\>, outputReference\\: array\\{type\\: class\\-string, payload\\: array\\<string, mixed\\>\\}, methodName\\: string, deprecationReason\\: string\\|null, deferredTypeLoader\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Type\\\\Loader\\\\DeferredTypeLoader\\>\\|null\\} but returns array\\{className\\: class\\-string, name\\: string, description\\: string\\|null, argumentNodes\\: list\\<array\\{node\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\Child\\\\ArgumentNode\\>&literal\\-string, payload\\: array\\<string, mixed\\>\\}\\>, outputReference\\: array\\{type\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Node\\\\TypeReference\\\\TypeReference\\>&literal\\-string, payload\\: array\\<string, mixed\\>\\}, methodName\\: string, deprecationReason\\: string\\|null, deferredTypeLoader\\: class\\-string\\<Jerowork\\\\GraphqlAttributeSchema\\\\Type\\\\Loader\\\\DeferredTypeLoader\\>\\|null\\}\\.$#',
3636
'identifier' => 'return.type',
3737
'count' => 1,
3838
'path' => __DIR__ . '/src/Node/QueryNode.php',
@@ -97,6 +97,12 @@
9797
'count' => 1,
9898
'path' => __DIR__ . '/src/NodeParser/Child/CursorNodeParser.php',
9999
];
100+
$ignoreErrors[] = [
101+
'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\NodeParser\\\\Child\\\\MapContextNodeParser\\:\\:getAttribute\\(\\) has parameter \\$reflector with generic class ReflectionClass but does not specify its types\\: T$#',
102+
'identifier' => 'missingType.generics',
103+
'count' => 1,
104+
'path' => __DIR__ . '/src/NodeParser/Child/MapContextNodeParser.php',
105+
];
100106
$ignoreErrors[] = [
101107
'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\NodeParser\\\\EnumNodeParser\\:\\:getAttribute\\(\\) has parameter \\$reflector with generic class ReflectionClass but does not specify its types\\: T$#',
102108
'identifier' => 'missingType.generics',

src/Attribute/MapContext.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Jerowork\GraphqlAttributeSchema\Attribute;
6+
7+
use Attribute;
8+
9+
/**
10+
* Maps a resolver method parameter to an object stored in the GraphQL Context.
11+
*
12+
* The object is fetched from the Context by the parameter's type. A nullable parameter resolves to
13+
* null when the object is absent; a non-nullable parameter throws when it is absent.
14+
*/
15+
#[Attribute(Attribute::TARGET_PARAMETER)]
16+
final readonly class MapContext {}

0 commit comments

Comments
 (0)