Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 4 additions & 0 deletions docs/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ $server = new StandardServer(ServerConfig::create([
]));
```

💡 *Want to pass per-request data (the HTTP request, the current user, …) to your resolvers? Set a*
`Context` *as the server's context value and read it with* `#[MapContext]`*. See*
[#[MapContext]](usage.md#mapcontext) *and* [Context](usage.md#context).

📌 *This library does not create a GraphQL server for you.*

To learn how to set up a server, check the [webonyx documentation](https://webonyx.github.io/graphql-php/executing-queries/#using-server).
Expand Down
131 changes: 131 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ You can use the following attributes:
- [#[Field]](#field)
- [#[Arg]](#arg)
- [#[Autowire]](#autowire)
- [#[MapContext]](#mapcontext)
- [#[Scalar]](#scalar)
- [#[Cursor]](#cursor)

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

It also works on `#[Query]` and `#[Mutation]` methods. Those resolvers are themselves resolved from the DI container,
so they can usually receive dependencies through their constructor — but `#[Autowire]` is available there too when you
prefer to inject a service per method.

```php
use Jerowork\GraphqlAttributeSchema\Attribute\Autowire;
use Jerowork\GraphqlAttributeSchema\Attribute\Type;
Expand Down Expand Up @@ -492,6 +497,132 @@ By default, the service to inject is determined by the parameter type. If needed
|-----------|------------------------------------------------------------------------------------|
| `service` | *(Optional)* Custom service identifier to retrieve from the DI container (PSR-11). |

### #[MapContext]

Webonyx/graphql-php passes a **context value** to every resolver. This library wraps it in a
`Context` object (see [Context](#context)) that you can populate per request — for example with the
current HTTP request, the authenticated user, or any value resolved higher up in the query tree.

`#[MapContext]` injects an object stored in that `Context` into a resolver parameter, looked up by the
parameter's type. It works on `#[Query]`, `#[Mutation]`, and `#[Field]` method parameters.

```php
use Jerowork\GraphqlAttributeSchema\Attribute\MapContext;
use Jerowork\GraphqlAttributeSchema\Attribute\Query;
use Symfony\Component\HttpFoundation\Request;

final readonly class YourQuery
{
#[Query]
public function yourQuery(
int $filter,
#[MapContext]
Request $request,
): YourType {
// $request is fetched from the Context by its class
}
}
```

A resolver can also attach objects to the `Context` so descendant fields can read them through
`#[MapContext]`. Because `Context` is a webonyx `ScopedContext`, children receive a clone, so values
attached by a resolver are visible to its descendants without leaking to sibling fields:

```php
#[Field]
public function getChild(Context $context): ChildType
Comment thread
jerowork marked this conversation as resolved.
{
$context->attachObject(new Locale('en'));
// ... children resolving under this field can now `#[MapContext] Locale $locale`
}
```

#### Nullable parameters

- A **non-nullable** parameter (`Request $request`) throws a `ContextException` when no matching object
is present in the `Context`.
- A **nullable** parameter (`?Request $request`) resolves to `null` when no matching object is present.

#### Requirements

- The parameter must be type-hinted with a class (not a built-in type).
- The GraphQL context value **must** be an instance of
`Jerowork\GraphqlAttributeSchema\Context`. If it is anything else, a `ContextException` is thrown when
the parameter is resolved. See [Context](#context) for how to set this up on your server.

### Context

`Jerowork\GraphqlAttributeSchema\Context` is the per-request context value passed to your resolvers. It
stores objects keyed by their class and also implements `ArrayAccess`, so you can use it as a typed
container or as a simple map.

There are two ways to read it from a resolver, and they do different things:

- **Type-hint `Context $context`** (no attribute) — the resolver receives the **whole `Context` object**.
This is built into the framework: any parameter typed `Context` is recognised automatically. Use it to
read multiple values, or to `attachObject()` something for descendant fields.
- **`#[MapContext] Request $request`** — the resolver receives **a single item out of the `Context`**,
looked up by the parameter's type (here `Request`). See [#[MapContext]](#mapcontext).

```php
use Jerowork\GraphqlAttributeSchema\Attribute\MapContext;
use Jerowork\GraphqlAttributeSchema\Context;
use Symfony\Component\HttpFoundation\Request;

#[Query]
public function yourQuery(
Context $context, // the whole Context object
#[MapContext]
Request $request, // just the Request stored in the Context
): YourType {
// ...
}
```

The `Context` object itself:

```php
use Jerowork\GraphqlAttributeSchema\Context;

$context = new Context();

// Attach by class
$context->attachObject($request); // stored under Request::class
$request = $context->getObject(Request::class); // throws if missing/wrong type
$request = $context->maybeGetObject(Request::class); // null if missing

// Or as an array (any key)
$context[Request::class] = $request;
$locale = $context['locale'] ?? null;
```

Set it on your server through the webonyx `context` option. Using a callable means a fresh `Context`
is built per request:

```php
use GraphQL\Error\DebugFlag;
use GraphQL\Server\ServerConfig;
use GraphQL\Server\StandardServer;
use Jerowork\GraphqlAttributeSchema\Context;
use Symfony\Component\HttpFoundation\Request;

$config = [
'schema' => $this->createSchema(),
'context' => function () {
$context = new Context();
$context[Request::class] = $this->requestStack->getMainRequest();

return $context;
},
];

if ($this->environment !== EnvironmentName::PROD) {
$config['debugFlag'] = DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE | DebugFlag::RETHROW_INTERNAL_EXCEPTIONS;
}

return new StandardServer(ServerConfig::create($config));
```

### #[Scalar]

Webonyx/graphql-php comes with four built-in scalar types:
Expand Down
12 changes: 9 additions & 3 deletions phpstan-baseline.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
'path' => __DIR__ . '/src/Node/Child/ArgNode.php',
];
$ignoreErrors[] = [
'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, \\.\\.\\.\\}\\.$#',
'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, \\.\\.\\.\\}\\.$#',
'identifier' => 'return.type',
'count' => 1,
'path' => __DIR__ . '/src/Node/Child/FieldNode.php',
Expand All @@ -20,7 +20,7 @@
'path' => __DIR__ . '/src/Node/Child/FieldNode.php',
];
$ignoreErrors[] = [
'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\\}\\.$#',
'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\\}\\.$#',
'identifier' => 'return.type',
'count' => 1,
'path' => __DIR__ . '/src/Node/MutationNode.php',
Expand All @@ -32,7 +32,7 @@
'path' => __DIR__ . '/src/Node/MutationNode.php',
];
$ignoreErrors[] = [
'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\\}\\.$#',
'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\\}\\.$#',
'identifier' => 'return.type',
'count' => 1,
'path' => __DIR__ . '/src/Node/QueryNode.php',
Expand Down Expand Up @@ -97,6 +97,12 @@
'count' => 1,
'path' => __DIR__ . '/src/NodeParser/Child/CursorNodeParser.php',
];
$ignoreErrors[] = [
'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\NodeParser\\\\Child\\\\MapContextNodeParser\\:\\:getAttribute\\(\\) has parameter \\$reflector with generic class ReflectionClass but does not specify its types\\: T$#',
'identifier' => 'missingType.generics',
'count' => 1,
'path' => __DIR__ . '/src/NodeParser/Child/MapContextNodeParser.php',
];
$ignoreErrors[] = [
'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\NodeParser\\\\EnumNodeParser\\:\\:getAttribute\\(\\) has parameter \\$reflector with generic class ReflectionClass but does not specify its types\\: T$#',
'identifier' => 'missingType.generics',
Expand Down
16 changes: 16 additions & 0 deletions src/Attribute/MapContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Jerowork\GraphqlAttributeSchema\Attribute;

use Attribute;

/**
* Maps a resolver method parameter to an object stored in the GraphQL Context.
*
* The object is fetched from the Context by the parameter's type. A nullable parameter resolves to
* null when the object is absent; a non-nullable parameter throws when it is absent.
*/
#[Attribute(Attribute::TARGET_PARAMETER)]
final readonly class MapContext {}
Loading