uxf/graphql

3.61.4 2025-02-22 22:50 UTC

This package is auto-updated.

Last update: 2025-02-22 21:54:46 UTC


README

Install

$ composer req uxf/graphql

Config

// config/packages/uxf.php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    $containerConfigurator->extension('uxf_graphql', [
        'destination' => __DIR__ . '/../generated/schema.graphql', // or array
        'fake' => [
            'enabled' => true, // default false
            'default_strategy' => true, // default false
            'namespace' => 'App\\Tests\\TInput', // optional
            'destination' => __DIR__ . '/../generated/fake', // optional
        ],
        'sources' => [
            __DIR__ . '/../src',
        ],
        'injected' => [
            // class => resolver
            Coyote::class => CoyoteResolver::class,
        ],
        'modifiers' => [
            AppTypeMapModifier::class,
        ],
        'hydrator_options' => [
            'allow_trim_string' => false, // default true
            'nullable_optional' => false, // default true
        ],
        'register_psr_17' => false, // default true
        'debug_flag' => DebugFlag::INCLUDE_DEBUG_MESSAGE, // default
    ]);
};

Query

use UXF\GraphQL\Attribute\Query;

final readonly class ArticlesQuery
{
    public function __construct(
        private ArticleProvider $articleProvider
    ) {
    }

    /**
     * @return ArticleType[]
     */
    #[Query('articles')]
    public function __invoke(int $limit, int $offset): array
    {
        return $this->articleProvider->list($limit, $offset);
    }
}
query Articles($limit: Int!, $offset: Int!) {
    articles(limit: $limit, offset: $offset) {
        id
        name
    }
}

Mutation

use UXF\GraphQL\Attribute\Mutation;

final readonly class ArticleCreateMutation
{
    public function __construct(
        private ArticleCreator $articleCreator,
        private ArticleProvider $articleProvider,
    ) {
    }

    #[Mutation('articleCreate')]
    public function __invoke(ArticleInput $input): ArticleType
    {
        $article = $this->articleCreator->create($input);
        return $this->articleProvider->get($article);
    }
}
mutation ArticleCreate($input: ArticleInput!) {
    articleCreate(input: $input) {
        id
        name
    }
}

Type

use UXF\GraphQL\Attribute\Type;

#[Type('Article')]
final readonly class ArticleType
{
    public function __construct(
        private Article $article,
        public int $id,
        public string $name,
    ) {
    }

    /**
     * @return TagType[]
     */
    public function tags(): array
    {
        return $this->article->getTags()->map(TagType::create(...))->getValues();
    }
}
type Article {
    id: Int
    name: String
    tags: [Tag!]!
}

Input

use UXF\GraphQL\Attribute\Input;

#[Input('ArticleInput')]
final readonly class ArticleInput
{
    /**
     * @param int[] $tags
     */
    public function __construct(
        public string $name,
        public ?Date $published,
        public array $tags,
        public int $score = 1,
    ) {
    }
}
input ArticleInput {
    name: String!
    published: Date
    tags: [Int!]!
    score: Int = 1
}

Validation

use Symfony\Component\Validator\Constraints as Assert;
use UXF\GraphQL\Attribute\Input;

#[Input]
final readonly class ValidationInput
{
    public function __construct(
        #[Assert\NotBlank] public string $string,
        #[Assert\Positive] public int $number,
    ) {
    }
}

NotSet


use UXF\Core\Http\Request\NotSet;
use UXF\GraphQL\Attribute\Input;

#[Input]
final readonly class PatchInput
{
    /**
     * @param int[]|null|NotSet $intArray
     */
    public function __construct(
        public string|null|NotSet $string = new NotSet(),
        public array|null|NotSet $intArray = new NotSet(),
    ) {
    }
}

input PatchInput {
  string: String
  intArray: [Int!]
}

Generate fake input

use App\Entity\Donald;
use UXF\Core\Attribute\Entity;
use UXF\GraphQL\Attribute\Input;

#[Input(generateFake: true)]
final readonly class DoctrineInput
{
    /**
     * @param Donald[] $donalds
     */
    public function __construct(
        #[Entity] public Donald $donald,
        #[Entity] public array $donalds,
    ) {
    }
}

with

    $containerConfigurator->extension('uxf_graphql', [
        'fake' => [
            'enabled' => true,
            'default_strategy' => false,
            'namespace' => 'App\\Tests\\FakeInput',
            'destination' => __DIR__ . '/../tests/FakeInput',
        ],
        ...
    ]);

generates

namespace App\Tests\FakeInput;

final readonly class FakeDoctrineInput
{
    /**
     * @param int[] $donalds
     */
    public function __construct(
        public int $donald,
        public array $donalds,
    ) {
    }
}

Entity

Arguments

use App\Entity\Donald;
use UXF\Core\Attribute\Entity;
use UXF\GraphQL\Attribute\Query;

final readonly class DonaldQuery
{
    /**
     * @param Donald[] $donalds
     */
    #[Query('donald')]
    public function __invoke(
        #[Entity] Donald $donald,
        #[Entity('uuid')] Donald $donald2,
        #[Entity] array $donalds,
    ): DonaldType {
        ...
    }
}
query Donald($donald: Int!, $donald2: UUID!, $donalds: [Int!]!) {
    donald(
        donald: $donald
        donald2: $donald2
        donalds: $donalds
    ) {
        id
        name
    }
}

Input

use App\Entity\Donald;
use UXF\Core\Attribute\Entity;
use UXF\GraphQL\Attribute\Input;

#[Input]
final readonly class DoctrineInput
{
    /**
     * @param Donald[] $donalds
     * @param Donald[] $donaldNames
     * @param Donald[] $donaldUuids
     * @param Donald[] $donaldRefs
     * @param Donald[]|null $donaldsNullable
     * @param Donald[]|null $donaldsNullableOptional
     * @param Donald[] $donaldsOptional
     */
    public function __construct(
        #[Entity] public Donald $donald,
        #[Entity('name')] public Donald $donaldName,
        #[Entity('uuid')] public Donald $donaldUuid,
        #[Entity('minnie')] public Donald $donaldRef,
        #[Entity('enum')] public Donald $donaldEnum,
        #[Entity('enumInt')] public Donald $donaldEnumInt,
        #[Entity] public array $donalds,
        #[Entity('name')] public array $donaldNames,
        #[Entity('uuid')] public array $donaldUuids,
        #[Entity('minnie')] public array $donaldRefs,
        #[Entity('name')] public ?Donald $donaldNullable,
        #[Entity('uuid')] public ?array $donaldsNullable,
        #[Entity('name')] public ?Donald $donaldNullableOptional = null,
        #[Entity('name')] public ?array $donaldsNullableOptional = null,
        #[Entity('name')] public array $donaldsOptional = [],
    ) {
    }
}
input DoctrineInput {
  donald: Int!
  donaldName: String!
  donaldUuid: UUID!
  donaldRef: Int!
  donaldEnum: PlutoEnum!
  donaldEnumInt: GoofyEnum!
  donalds: [Int!]!
  donaldNames: [String!]!
  donaldUuids: [UUID!]!
  donaldRefs: [Int!]!
  donaldNullable: String
  donaldsNullable: [UUID!]
  donaldNullableOptional: String = null
  donaldsNullableOptional: [String!] = null
  donaldsOptional: [String!]! = []
}

Union

Property

use UXF\GraphQL\Attribute\Type;
use UXF\GraphQL\Attribute\Union;

#[Type('Bag')]
final readonly class BagType
{
    public function __construct(
        #[Union('Fruit')] public Apple|Banana $item,
    ) {
    }
}
union Fruit = Apple | Banana

type Bag {
    item: Fruit!
}

Method

use UXF\GraphQL\Attribute\Type;
use UXF\GraphQL\Attribute\Union;

#[Type('Bag')]
final readonly class BagType
{
    /**
     * @return (Apple|Banana)[]
     */
    #[Union('Fruit')]
    public function getItems(): array
    {
        return [new Banana(2, 'C')];
    }
}
union Fruit = Apple | Banana

type Bag {
    items: [Fruit!]!
}

Inject

  • Symfony Request injector is registered by default

Query/Mutation

use UXF\GraphQL\Attribute\Inject;

final readonly class PlutoQuery
{
    #[Query(name: 'pluto')]
    public function __invoke(#[Inject] Coyote $coyote): Pluto
    {
        ...
    }
}

Type method

use UXF\GraphQL\Attribute\Inject;

#[Type('Pluto')]
final readonly class PlutoQuery
{
    /**
     * @return string[]
     */
    public function getItems(#[Inject] Coyote $coyote): array
    {
        ...
    }
}

Injector service

use Symfony\Component\HttpFoundation\Request;
use UXF\GraphQL\Service\Injector\InjectedArgument;

final readonly class CoyoteInjector
{
    public function __invoke(Request $request, InjectedArgument $argument): Coyote
    {
        assert($argument->type === Coyote::class);
        return new Coyote();
    }
}

Autowire

use UXF\GraphQL\Attribute\Autowire;

#[Type('Pluto')]
final readonly class PlutoQuery
{
    /**
     * @return string[]
     */
    public function getItems(#[Autowire] ResponseProvider $provider): array
    {
        ...
    }
}
type Pluto {
    items: [String!]!
}

Field

Input

use UXF\GraphQL\Attribute\Field;

#[Input]
final readonly class PlutoInput
{
    public function __construct(
        #[Field(inputType: 'Long')] public int $long,
    ) {
    }
}
input PlutoInput {
    long: Long!
}

Output

use UXF\GraphQL\Attribute\Field;

#[Type]
final readonly class Pluto
{
    public function __construct(
        public string $string,
        #[Field(outputType: 'Long')] public int $long,
    ) {
    }

    #[Field(outputType: 'Long')]
    public function getLong2(): int
    {
        return -1;
    }
}
type Pluto {
    string: String!
    long: Long!
    long2: Long!
}

Ignore

use UXF\GraphQL\Attribute\Ignore;

#[Type]
final readonly class Pluto
{
    public function __construct(
        public string $string,
        #[Ignore] public bool $ignoredProperty,
    ) {
    }

    #[Ignore]
    public function ignored(): bool
    {
        return true;
    }
}
type Pluto {
    string: String!
}

SchemaModifier

// config/packages/uxf.php

return static function (ContainerConfigurator $containerConfigurator): void {
    $containerConfigurator->extension('uxf_graphql', [
        'modifiers' => [
            AppTypeMapModifier::class,
        ],
        ...
    ]);
};
use UXF\GraphQL\Plugin\SchemaModifier;

final class AppSchemaModifier implements SchemaModifier
{
    // add custom types
    public static function modifyTypeMap(TypeMap $typeMap): void
    {
        // simple scalar
        $typeMap->scalars[ResultTime::class] = new ScalarTypeSchema(
            name: 'ResultTime',
            phpType: ResultTime::class,
            definition: new ScalarDefinition(ResultTimeType::class),
        );

        // scalar with dependency
        $typeMap->scalars[SuperScalar::class] = new ScalarTypeSchema(
            name: 'SuperScalar',
            phpType: SuperScalar::class,
            definition: new ScalarDefinition(SuperScalarType::class, 'int'),
        );
    }

    // modify input parse function
    public static function modifyParseValueFn(InputTypeSchema $inputType): ?string
    {
        if ($inputType->phpType === Money::class) {
            return 'function (array $values) {
    try {
        return \\' . Money::class . "::of(\$values['amount'], \$values['currency']);
    } catch (\Exception) {
        throw new Error('Invalid money format', previous: new UserError('Invalid money format'));
    }
}";
        }

        return null;
    }

    public static function getDefaultPriority(): int
    {
        return 10;
    }
}