wwwision/types-graphql

1.3.0 2024-02-29 16:51 UTC

This package is auto-updated.

Last update: 2024-04-29 17:12:45 UTC


README

Integration for the wwwision/types package that allows to generate GraphQL schemas from PHP code

Usage

This package can be installed via composer:

composer require wwwision/types-graphql

To generate a GraphQL schema, create a class with at least one public method with a Query attribute:

final class SomeApi {

    #[Query]
    public function ping(string $input): string
    {
        return strtoupper($input);
    }
}

Now, this API can be used to create a GraphQL schema:

// ...
$generator = new GraphQLGenerator();
$schema = $generator->generate(SomeApi::class)->render();

assert($schema === 'type Query {
  ping(input: String!): String!
}
');

Advanced types

If you API class refers to more complex types (using attributes from the wwwision/types package) in method parameters or return types, they will be added to the schema too.

Example: Advanced types
  1. Given you have the following classes defined:
#[StringBased]
final class GivenName {
    private function __construct(public readonly string $value) {}
}

#[StringBased]
final class FamilyName {
    private function __construct(public readonly string $value) {}
}

final class FullName {
    public function __construct(
        public readonly GivenName $givenName,
        public readonly FamilyName $familyName,
    ) {}
}

#[Description('honorific title of a person')]
enum HonorificTitle
{
    #[Description('for men, regardless of marital status, who do not have another professional or academic title')]
    case MR;
    #[Description('for married women who do not have another professional or academic title')]
    case MRS;
    #[Description('for girls, unmarried women and married women who continue to use their maiden name')]
    case MISS;
    #[Description('for women, regardless of marital status or when marital status is unknown')]
    case MS;
    #[Description('for any other title that does not match the above')]
    case OTHER;
}

#[Description('A contact in the system')]
final class Contact {
    public function __construct(
        public readonly HonorificTitle $title,
        public readonly FullName $name,
        #[Description('Whether the contact is registered or not')]
        public bool $isRegistered = false,
    ) {}
}

#[ListBased(itemClassName: Contact::class)]
final class Contacts {
    private function __construct(private readonly array $contacts) {}
}
  1. ...and this API class:
// ...
final class SomeApi {

    #[Query]
    public function findContactsByFamilyName(FamilyName $familyName): Contacts
    {
        // ...
    }

    #[Mutation]
    public function addContact(Contact $newContact): bool
    {
        // ...
    }

}
  1. The GraphQL schema is more verbose now:
// ...
$generator = new GraphQLGenerator();
$schema = $generator->generate(SomeApi::class)->render();

$expectedSchema = <<<GRAPHQL
type Query {
  findContactsByFamilyName(familyName: FamilyName!): [Contact!]!
}

type Mutation {
  addContact(newContact: ContactInput!): Boolean!
}

scalar FamilyName

"""
honorific title of a person
"""
enum HonorificTitle {
  """
  for men, regardless of marital status, who do not have another professional or academic title
  """
  MR
  """
  for married women who do not have another professional or academic title
  """
  MRS
  """
  for girls, unmarried women and married women who continue to use their maiden name
  """
  MISS
  """
  for women, regardless of marital status or when marital status is unknown
  """
  MS
  """
  for any other title that does not match the above
  """
  OTHER
}

scalar GivenName

type FullName {
  givenName: GivenName!
  familyName: FamilyName!
}

type Contact {
  """ honorific title of a person """
  title: HonorificTitle!
  name: FullName!
  """ Whether the contact is registered or not """
  isRegistered: Boolean
}

input FullNameInput {
  givenName: GivenName!
  familyName: FamilyName!
}

input ContactInput {
  """ honorific title of a person """
  title: HonorificTitle!
  name: FullNameInput!
  """ Whether the contact is registered or not """
  isRegistered: Boolean
}

GRAPHQL;

assert($schema === $expectedSchema);

Type constraints

The GraphQL schema does not have a notion of advanced type constraints. But the GraphQLGenerator can turn them into graphql-constraint directives that can be interpreted by consumers. It also adds the constraint rules to descriptions of the corresponding fields.

Example: Type constraints
#[StringBased(minLength: 1, maxLength: 200)]
final class Name {
    private function __construct(public readonly string $value) {}
}

#[IntegerBased(minimum: 1, maximum: 130)]
final class Age {
    private function __construct(public readonly int $value) {}
}

#[ListBased(itemClassName: Name::class, minCount: 1, maxCount: 5)]
final class Names {
    private function __construct(private readonly array $names) {}
}

final class SomeApi {
    #[Query]
    public function oldestPerson(Names $someNames): ?Age
    {
        // ...
    }
}

$generator = new GraphQLGenerator();
$schema = $generator->generate(SomeApi::class)->render();

$expectedSchema = <<<GRAPHQL
"""
Custom constraint directive (see https://www.npmjs.com/package/graphql-constraint-directive)
"""
directive @constraint(minLength: Int maxLength: Int pattern: String format: String min: Int max: Int minItems: Int maxItems: Int) on FIELD_DEFINITION | SCALAR | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION


type Query {
  oldestPerson(someNames: [Name!]! @constraint(minItems: 1 maxItems: 5)): Age
}

"""

*Constraints:*
* Minimum length: `1`
* Maximum length: `200`
"""
scalar Name @constraint(minLength: 1 maxLength: 200)

"""

*Constraints:*
* Minimum value: `1`
* Maximum value: `130`
"""
scalar Age @constraint(min: 1 max: 130)

GRAPHQL;

assert($schema === $expectedSchema);

Custom resolvers

With version 1.2.0 custom functions can be registered that extend the behavior of types dynamically.

Note The signature of the custom resolver closure has to contain the extended type as first argument and specify the return type, for example: new CustomResolver('SomeObject', 'someCustomField', fn (SomeObject $thisIsRequired, string $thisIsOptional): bool => true)

Example: Custom resolvers
final class User {
    public function __construct(
        public readonly string $givenName,
        public readonly string $familyName,
    ) {}
}

#[ListBased(itemClassName: User::class)]
final class Users {
}

final class SomeApi {
    #[Query]
    public function users(): ?Users
    {
        // ...
    }
}

$generator = new GraphQLGenerator();
$customResolvers = CustomResolvers::create(new CustomResolver('User', 'fullName', fn (User $user): string => $user->givenName . ' ' . $user->familyName));
$schema = $generator->generate(SomeApi::class, $customResolvers)->render();

$expectedSchema = <<<GRAPHQL
type Query {
  users: [User!]
}

type User {
  givenName: String!
  familyName: String!
  fullName: String!
}

GRAPHQL;

assert($schema === $expectedSchema);

Contribution

Contributions in the form of issues or pull requests are highly appreciated

License

See LICENSE