Skip to content

Commit

Permalink
Introduce #[Autowire]
Browse files Browse the repository at this point in the history
To inject services in #[Type] methods.
  • Loading branch information
jerowork committed Jan 8, 2025
1 parent 74c145d commit 4e8819a
Show file tree
Hide file tree
Showing 31 changed files with 577 additions and 115 deletions.
2 changes: 1 addition & 1 deletion docs/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This library is still work in progress, and misses some valuable features:
- ~~Allow simple lists (array type)~~
- ~~Make AST serializable (cacheable)~~
- ~~Handle `DateTime` and `DateTimeImmutable`~~
- ~~Inject autowiring services~~
- Connection, edge, nodes (see https://relay.dev/graphql/connections.htm)
- GraphQL interfaces, inheritance
- Inject autowiring services
- Subscriptions
60 changes: 58 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The following attributes can be used:
- [#[EnumValue]](#enum)
- [#[Field]](#field)
- [#[Arg]](#arg)
- [#[Autowire]](#autowire)
- [#[Scalar]](#scalar)

See below for more information about each attribute:
Expand Down Expand Up @@ -235,7 +236,8 @@ In `#[Type]` and `#[InputType]`, to define fields, the `#[Field]` attribute can
In order to configure any fields this can be set on constructor property (for `#[InputType]` or `#[Type]`) or
on method (for `#[Type]` only).

The advantage to set on methods for `#[Type]` is that the method can have input arguments as well (e.g. filtering).
The advantage to set on methods for `#[Type]` is that the method can have input arguments as well (e.g. filtering,
injected services).

```php
use Jerowork\GraphqlAttributeSchema\Attribute\Field;
Expand Down Expand Up @@ -343,8 +345,57 @@ final readonly class YourType
| `description` | Set description of the argument, readable in the GraphQL schema |
| `type` | Set custom return type; it can be:<br/>- A Type (FQCN)<br/>- A `ScalarType` (e.g. `ScalarType::Int`)<br/>- A `ListType` (e.g. `new ListType(ScalarType::Int)`)<br/>- A `NullableType` (e.g. `new NullableType(SomeType::class)`)<br/>- A combination of `ListType` and `NullableType` and a Type FQCN or `ScalarType` <br/>(e.g. `new NullableType(new ListType(ScalarType::String))`) |

### #[Autowire]

`#[Type]` objects are typically modeled like DTO's. They are often not defined in any DI container.
Using other services inside a `#[Type]` is therefore not so easy.

This is where `#[Autowire]` comes into play. `#[Type]` methods defined with `#[Field]` can inject services by parameter
by autowiring, with `#[Autowire]`.

```php
use Jerowork\GraphqlAttributeSchema\Attribute\Autowire;
use Jerowork\GraphqlAttributeSchema\Attribute\Type;

#[Type]
final readonly class YourType
{
public function __construct(
...
) {}

public function getFoobar(
int $filter,
#[Autowire]
SomeService $service,
) {
// .. use injected $service
}
}
```

#### Automatic schema creation

Which service to inject, is automatically defined by the type of the parameter.
This can be overwritten by the option `service`, see options section below.

#### Requirements

Autowired services:

- must be retrievable from the container (`get()`); especially for Symfony users, these should be set to public (e.g.
with `#[Autoconfigure(public: true)]`),

#### Options

| Option | Description |
|-----------|---------------------------------------------------------------------------------|
| `service` | (optional) Set custom service identifier to retrieve from DI Container (PSR-11) |

### #[Scalar]

Webonyx/graphql-php supports 4 native scalar types:

- string
- integer
- boolean
Expand Down Expand Up @@ -373,21 +424,26 @@ final readonly class CustomScalar implements ScalarType
}
```

This custom scalar type can then be defined as type with option `type` within other attributes (e.g. `#[Field]`, `#[Mutation]`).
This custom scalar type can then be defined as type with option `type` within other attributes (e.g. `#[Field]`,
`#[Mutation]`).
The `type` option can be omitted when using `alias` in `#[Scalar]`, see options section below.

#### Requirements

Custom scalar types:

- must implement `ScalarType`.

#### Options

| Option | Description |
|---------------|-----------------------------------------------------------------------------------|
| `name` | Set custom name of scalar type (instead of based on class) |
| `description` | Set description of the scalar type, readable in the GraphQL schema |
| `alias` | Map scalar type to another class, which removes the need to use the `type` option |

#### Custom ScalarType: DateTimeImmutable

*GraphQL Attribute Schema* already has a custom scalar type built-in: [DateTimeType](../src/Type/DateTimeType.php).

With this custom type, `DateTimeImmutable` can be used out-of-the-box (without any `type` option definition).
Expand Down
18 changes: 18 additions & 0 deletions src/Attribute/Autowire.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Jerowork\GraphqlAttributeSchema\Attribute;

use Attribute;

#[Attribute(Attribute::TARGET_PARAMETER)]
final readonly class Autowire
{
/**
* @param string|class-string $service
*/
public function __construct(
public ?string $service = null,
) {}
}
39 changes: 39 additions & 0 deletions src/Parser/Node/Child/AutowireNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Jerowork\GraphqlAttributeSchema\Parser\Node\Child;

use Jerowork\GraphqlAttributeSchema\Parser\Node\ArraySerializable;

/**
* @phpstan-type AutowireNodePayload array{
* service: string|class-string,
* propertyName: string,
* }
*
* @implements ArraySerializable<AutowireNodePayload>
*/
final readonly class AutowireNode implements ArraySerializable
{
public function __construct(
public string $service,
public string $propertyName,
) {}

public function toArray(): array
{
return [
'service' => $this->service,
'propertyName' => $this->propertyName,
];
}

public static function fromArray(array $payload): AutowireNode
{
return new self(
$payload['service'],
$payload['propertyName'],
);
}
}
34 changes: 29 additions & 5 deletions src/Parser/Node/Child/FieldNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@

/**
* @phpstan-import-type ArgNodePayload from ArgNode
* @phpstan-import-type AutowireNodePayload from AutowireNode
* @phpstan-import-type TypePayload from Type
*
* @phpstan-type FieldNodePayload array{
* type: TypePayload,
* name: string,
* description: null|string,
* argNodes: list<ArgNodePayload>,
* argumentNodes: list<array{
* node: class-string<ArgNode|AutowireNode>,
* payload: ArgNodePayload|AutowireNodePayload
* }>,
* fieldType: string,
* methodName: null|string,
* propertyName: null|string,
Expand All @@ -27,13 +31,13 @@
final readonly class FieldNode implements ArraySerializable
{
/**
* @param list<ArgNode> $argNodes
* @param list<ArgNode|AutowireNode> $argumentNodes
*/
public function __construct(
public Type $type,
public string $name,
public ?string $description,
public array $argNodes,
public array $argumentNodes,
public FieldNodeType $fieldType,
public ?string $methodName,
public ?string $propertyName,
Expand All @@ -42,11 +46,19 @@ public function __construct(

public function toArray(): array
{
$argumentNodes = [];
foreach ($this->argumentNodes as $argumentNode) {
$argumentNodes[] = [
'node' => $argumentNode::class,
'payload' => $argumentNode->toArray(),
];
}

return [
'type' => $this->type->toArray(),
'name' => $this->name,
'description' => $this->description,
'argNodes' => array_map(fn($argNode) => $argNode->toArray(), $this->argNodes),
'argumentNodes' => $argumentNodes,
'fieldType' => $this->fieldType->value,
'methodName' => $this->methodName,
'propertyName' => $this->propertyName,
Expand All @@ -56,11 +68,23 @@ public function toArray(): array

public static function fromArray(array $payload): FieldNode
{
$argumentNodes = [];
foreach ($payload['argumentNodes'] as $argumentNode) {
$argumentPayload = $argumentNode['payload'];
if ($argumentNode['node'] === ArgNode::class) {
/** @var ArgNodePayload $argumentPayload */
$argumentNodes[] = ArgNode::fromArray($argumentPayload);
} else {
/** @var AutowireNodePayload $argumentPayload */
$argumentNodes[] = AutowireNode::fromArray($argumentPayload);
}
}

return new self(
Type::fromArray($payload['type']),
$payload['name'],
$payload['description'],
array_map(fn($argNodePayload) => ArgNode::fromArray($argNodePayload), $payload['argNodes']),
$argumentNodes,
FieldNodeType::from($payload['fieldType']),
$payload['methodName'],
$payload['propertyName'],
Expand Down
35 changes: 35 additions & 0 deletions src/Parser/NodeParser/Child/ArgNodeParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Jerowork\GraphqlAttributeSchema\Parser\NodeParser\Child;

use Jerowork\GraphqlAttributeSchema\Attribute\Arg;
use Jerowork\GraphqlAttributeSchema\Parser\Node\Child\ArgNode;
use Jerowork\GraphqlAttributeSchema\Parser\NodeParser\GetTypeTrait;
use Jerowork\GraphqlAttributeSchema\Parser\NodeParser\ParseException;
use ReflectionParameter;

final readonly class ArgNodeParser
{
use GetTypeTrait;

/**
* @throws ParseException
*/
public function parse(ReflectionParameter $parameter, ?Arg $attribute): ArgNode
{
$type = $this->getType($parameter->getType(), $attribute);

if ($type === null) {
throw ParseException::invalidParameterType($parameter->getName());
}

return new ArgNode(
$type,
$attribute->name ?? $parameter->getName(),
$attribute?->description,
$parameter->getName(),
);
}
}
36 changes: 36 additions & 0 deletions src/Parser/NodeParser/Child/AutowireNodeParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Jerowork\GraphqlAttributeSchema\Parser\NodeParser\Child;

use Jerowork\GraphqlAttributeSchema\Attribute\Autowire;
use Jerowork\GraphqlAttributeSchema\Parser\Node\Child\AutowireNode;
use Jerowork\GraphqlAttributeSchema\Parser\NodeParser\ParseException;
use ReflectionParameter;
use ReflectionNamedType;

final readonly class AutowireNodeParser
{
/**
* @throws ParseException
*/
public function parse(ReflectionParameter $parameter, Autowire $attribute): AutowireNode
{
if ($attribute->service !== null) {
return new AutowireNode(
$attribute->service,
$parameter->getName(),
);
}

if (!$parameter->getType() instanceof ReflectionNamedType) {
throw ParseException::invalidAutowiredParameterType($parameter->getName());
}

return new AutowireNode(
$parameter->getType()->getName(),
$parameter->getName(),
);
}
}
2 changes: 1 addition & 1 deletion src/Parser/NodeParser/Child/ClassFieldNodesParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
private const array RESERVED_METHOD_NAMES = ['__construct'];

public function __construct(
private MethodArgNodesParser $methodArgNodesParser,
private MethodArgumentNodesParser $methodArgNodesParser,
) {}

/**
Expand Down
57 changes: 0 additions & 57 deletions src/Parser/NodeParser/Child/MethodArgNodesParser.php

This file was deleted.

Loading

0 comments on commit 4e8819a

Please sign in to comment.