📌 At a minimum, you need to define a query and a mutation to build a valid schema.
You can use the following attributes:
More details on each attribute are provided below.
You can define mutations and queries using #[Mutation]
and #[Query]
.
Simply add these attributes at the method level:
use Jerowork\GraphqlAttributeSchema\Attribute\Mutation;
use Jerowork\GraphqlAttributeSchema\Attribute\Query;
final readonly class YourMutation
{
#[Mutation]
public function mutationName(SomeInputType $input): OutputType {}
}
final readonly class YourQuery
{
#[Query]
public function queryName(string $id, int $status): string {}
}
GraphQL Attribute Schema automatically reads the method signature, including input arguments and output type.
These will be configured in the schema without additional setup (though you can override this using #[Arg]
, see
the Arg section).
Both input and output types can be scalars or objects. When using objects, make sure they’re properly defined using
#[InputType]
for input or #[Type]
for output. You can also use #[Enum]
for both input and output.
By default, the mutation or query name is taken from the method name, but you can override it (see options below).
Mutations and queries must:
Parser
(
see Getting Started > Integration with webonyx/graphql-php).get()
.#[Autoconfigure(public: true)]
).You can configure both #[Mutation]
and #[Query]
attributes:
Option | Description |
---|---|
name |
Custom name for the mutation or query (instead of using the method name). |
description |
Description of the mutation or query, visible in the GraphQL schema. |
type |
Custom return type, which can be: - A Type (FQCN) - A ScalarType (e.g., ScalarType::Int )- A ListType (e.g., new ListType(ScalarType::Int) )- A NullableType (e.g., new NullableType(SomeType::class) )- A combination of ListType , NullableType , and a Type FQCN or ScalarType (e.g., new NullableType(new ListType(ScalarType::String)) )- A UnionType (see Union types)- A ConnectionType (see Connections (Pagination)) |
deprecationReason |
Marks the mutation or query as deprecated if set. |
You can define types using #[Type]
. To configure a class as a type, simply add this attribute at the class level:
use Jerowork\GraphqlAttributeSchema\Attribute\Field;
use Jerowork\GraphqlAttributeSchema\Attribute\Type;
#[Type]
final readonly class YourType
{
public function __construct(
#[Field]
public int $id,
#[Field]
public ?string $name,
#[Field]
public AnotherType $anotherType,
#[Field]
public EnumType $enumType,
) {}
#[Field]
public function getStatus(): EnumStatusType {}
#[Field]
public function getFoobar(int $status, ?string $baz): EnumStatusType {}
}
GraphQL Attribute Schema automatically reads the __construct
signature and detects input arguments,
as well as method return types.
#[Field]
will be included in the schema by default (you can override this, see
the Field section).#[Field]
will be added to the schema.
#[Arg]
, see Arg).Like input types, types can be scalars or objects. If you’re using objects, ensure they’re properly defined with
#[InputType]
or #[Enum]
.
By default, the type name is taken from the class name, but you can override it (see options below).
You can configure the #[Type]
attribute:
Option | Description |
---|---|
name |
Custom name for the type (instead of using the class name). |
description |
Description of the type, visible in the GraphQL schema. |
GraphQL supports inheritance using interfaces. To configure an interface, simply add #[InterfaceType]
to a PHP interface or (abstract) class:
use Jerowork\GraphqlAttributeSchema\Attribute\Field;
use Jerowork\GraphqlAttributeSchema\Attribute\Type;
// Define an InterfaceType on interface
#[InterfaceType]
interface UserType
{
// With PHP 8.4, you can define fields using property hooks
#[Field]
public int $id { get; }
// For PHP versions below 8.4
#[Field]
public function getName(): ?string;
}
// Or define an InterfaceType on (abstract) class
#[InterfaceType]
abstract readonly class AbstractUserType
{
public function __construct(
#[Field]
public string $id,
) {}
#[Field]
abstract public function getName(): ?string;
}
Each implementation inherits all fields from the interface, in addition to its own fields:
use Jerowork\GraphqlAttributeSchema\Attribute\Field;
use Jerowork\GraphqlAttributeSchema\Attribute\Type;
#[Type]
final readonly class AgentType implements UserType
{
public function __construct(
#[Field]
public string $status,
public int $id, // No need to reapply #[Field] from interface
) {}
// No need to reapply #[Field] from interface
public function getName(): ?string
{
return '';
}
}
Other than a single #[Type]
implementing one interface #[InterfaceType]
as in the example above,
GraphQL Attribute Schema also supports multiple interfaces:
use Jerowork\GraphqlAttributeSchema\Attribute\Field;
use Jerowork\GraphqlAttributeSchema\Attribute\InterfaceType;
#[InterfaceType]
interface FooType
{
#[Field]
public function getFoo() : string;
}
#[InterfaceType]
interface BarType extends FooType
{
#[Field]
public function getBar() : string;
}
#[InterfaceType]
abstract readonly class AbstractBazType implements FooType
{
public function __construct(
#[Field]
public string $id,
) {}
#[Field]
abstract public function getBaz() : string;
}
#[Type]
final readonly class QuxType extends AbstractBazType
{
// Implement all required logic from interfaces/extends
}
You can configure the #[InterfaceType]
attribute:
Option | Description |
---|---|
name |
Custom name for the interface type (instead of using the interface/class name). |
description |
Description of the interface type, visible in the GraphQL schema. |
You can define input types using #[InputType]
. To configure a class as an input type, simply add this attribute at the
class level:
use Jerowork\GraphqlAttributeSchema\Attribute\Field;
use Jerowork\GraphqlAttributeSchema\Attribute\InputType;
#[InputType]
final readonly class YourInputType
{
public function __construct(
#[Field]
public int $id,
#[Field]
public ?string $name,
#[Field]
public AnotherInputType $anotherInputType,
#[Field]
public EnumType $enumType,
) {}
}
GraphQL Attribute Schema automatically reads the __construct
signature and detects input arguments.
Any argument marked with #[Field]
will be included in the schema by default (you can override this, see
the Field section).
Input values can be scalars or objects. If you’re using objects, ensure they’re properly defined using #[InputType]
or
#[Enum]
.
By default, the input type name is taken from the class name, but you can override it (see options below).
You can configure the #[InputType]
attribute:
Option | Description |
---|---|
name |
Custom name for the input type (instead of using the class name). |
description |
Description of the input type, visible in the GraphQL schema. |
You can define enums using #[Enum]
. To configure an enum class, simply add this attribute at the class level:
use Jerowork\GraphqlAttributeSchema\Attribute\Enum;
use Jerowork\GraphqlAttributeSchema\Attribute\EnumValue;
#[Enum]
enum YourEnumType: string
{
case Foo = 'FOO';
case Bar = 'BAR';
#[EnumValue(description: 'A description for case Baz')]
case Baz = 'BAZ';
}
GraphQL Attribute Schema automatically detects enum values from the PHP enum
,
using the string version of each case.
By default, the enum name is taken from the class name, but you can override it (see options below).
enum
type (no class-based constants).enum
must be a BackedEnum
.You can configure the #[Enum]
attribute:
Option | Description |
---|---|
name |
Custom name for the enum (instead of using the class name). |
description |
Description of the enum, visible in the GraphQL schema. |
Each enum case can also be configured using #[EnumValue]
:
Option | Description |
---|---|
description |
Description of the enum case, visible in the GraphQL schema. |
deprecationReason |
Marks the case as deprecated if set. |
The #[Field]
attribute is used in #[Type]
and #[InputType]
to define fields.
You can apply it to constructor properties (for #[InputType]
and #[Type]
) or methods (for #[Type]
only).
Using #[Field]
on methods in #[Type]
allows you to add input arguments (e.g., for filtering or injecting services).
use Jerowork\GraphqlAttributeSchema\Attribute\Field;
use Jerowork\GraphqlAttributeSchema\Attribute\InputType;
use Jerowork\GraphqlAttributeSchema\Attribute\Type;
#[InputType]
final readonly class YourInputType
{
// Applied to constructor properties
public function __construct(
#[Field]
public int $id,
#[Field]
public ?string $status,
) {}
}
#[Type]
final readonly class YourType
{
// Applied to constructor properties
public function __construct(
#[Field]
public int $id,
#[Field]
public ?string $status,
) {}
// Applied to methods
#[Field]
public function getFoobar(): string {}
#[Field]
public function getBaz(): ?EnumType {}
}
You can configure the #[Field]
attribute:
Option | Description |
---|---|
name |
Custom name for the field (instead of using the property/method name). |
description |
Description of the field, visible in the GraphQL schema. |
type |
Custom return type, which can be: - A Type (FQCN) - A ScalarType (e.g., ScalarType::Int )- A ListType (e.g., new ListType(ScalarType::Int) )- A NullableType (e.g., new NullableType(SomeType::class) )- A combination of ListType , NullableType , and a Type FQCN or ScalarType (e.g., new NullableType(new ListType(ScalarType::String)) )- A UnionType (see Union types)- A ConnectionType (see Connections (Pagination)) |
deprecationReason |
Marks the field as deprecated (only applicable in #[Type] ). |
When using #[Mutation]
, #[Query]
, or methods in #[Type]
that are marked with #[Field]
,
input arguments are automatically read from the method signature.
However, if you need to customize an argument (e.g., rename it), you can use #[Arg]
.
use Jerowork\GraphqlAttributeSchema\Attribute\Arg;
use Jerowork\GraphqlAttributeSchema\Attribute\Mutation;
use Jerowork\GraphqlAttributeSchema\Attribute\Query;
use Jerowork\GraphqlAttributeSchema\Attribute\Type;
#[Mutation]
final readonly class YourMutation
{
public function __construct(
public int $id,
#[Arg(name: 'customName')]
public ?string $name,
) {}
}
#[Query]
final readonly class YourQuery
{
public function __construct(
public int $id,
#[Arg(name: 'customName')]
public ?string $name,
) {}
}
#[Type]
final readonly class YourType
{
public function __construct(
...
) {}
public function getFoobar(
int $filter,
#[Arg(name: 'customName')]
?string $filter2,
) {}
}
You can configure the #[Arg]
attribute:
Option | Description |
---|---|
name |
Custom name for the argument (instead of using the parameter name). |
description |
Description of the argument, visible in the GraphQL schema. |
type |
Custom argument type, which can be: - A Type (FQCN) - A ScalarType (e.g., ScalarType::Int )- A ListType (e.g., new ListType(ScalarType::Int) )- A NullableType (e.g., new NullableType(SomeType::class) )- A combination of ListType , NullableType , and a Type FQCN or ScalarType (e.g., new NullableType(new ListType(ScalarType::String)) ). |
Objects in #[Type]
are usually structured like DTOs and are often not defined in the DI container.
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.
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 the injected $service
}
}
By default, the service to inject is determined by the parameter type. If needed, you can override this using the
service
option (see below).
get()
).#[Autoconfigure(public: true)]
).Option | Description |
---|---|
service |
(Optional) Custom service identifier to retrieve from the DI container (PSR-11). |
Webonyx/graphql-php comes with four built-in scalar types:
💡 Tip: Scalar types work for both input and output.
If you need something custom, you can define your own scalar type using #[Scalar]
:
use Jerowork\GraphqlAttributeSchema\Attribute\Scalar;
use Jerowork\GraphqlAttributeSchema\Type\ScalarType;
#[Scalar]
final readonly class CustomScalar implements ScalarType
{
public static function serialize(mixed $value): string
{
// ...
}
public static function deserialize(string $value): mixed
{
// ...
}
}
Once defined, you can use your custom scalar type in attributes like #[Field]
and #[Mutation]
.
If you use the alias
option in #[Scalar]
, the type
option becomes optional (see below).
Custom scalar types must:
ScalarType
interface.Option | Description |
---|---|
name |
Custom name for the scalar type (defaults to class name). |
description |
Description of the scalar type, visible in the GraphQL schema. |
alias |
Maps this scalar to another class, eliminating the need for type . |
GraphQL Attribute Schema includes a built-in custom scalar: DateTimeType.
This allows you to use DateTimeImmutable
out of the box—no extra type
definition needed!
If you’re building the Parser
with ParserFactory
, this type is registered automatically.
Otherwise, you can manually add DateTimeType
to the $customTypes
array in the Parser
constructor.
See Connections (Pagination) for details.
You can configure the #[Cursor]
attribute:
Option | Description |
---|---|
type |
Defines a custom return type. It can be: - A class implementing ScalarType (FQCN)- ScalarType::String - A NullableType wrapping one of the above (e.g., new NullableType(ScalarType::String) )All cursor values resolve to a string. |
GraphQL Attribute Schema allows union types as defined by the GraphQL specification.
“Union types share similarities with Interface types, but they cannot define any shared fields among the constituent types.”
See https://graphql.org/learn/schema/#union-types
By default, a GraphQL union type cannot not define any shared fields. Instead, it acts like a group of object types.
Therefore, in contrary to all other types, a GraphQL Attribute Schema union type cannot be defined by an attribute (as that would be an empty class/interface).
Instead, you can define a union type by (a) a native PHP union return type, or (b) a custom set type UnionType
in a #[Query]
or #[Mutation]
.
With using UnionType
, it is possible to define a custom name for the type. See below:
use Jerowork\GraphqlAttributeSchema\Attribute\Option\UnionType;
use Jerowork\GraphqlAttributeSchema\Attribute\Query;
final readonly class Query
{
// Define GraphQL union type by PHP return type only
#[Query]
public function getFoobar() : UserType|OtherType
{
// todo
}
// Or define GraphQL union type by type in attribute
#[Query(type: new UnionType('FoobarUnionType', UserType::class, OtherType::class))]
public function getFoobar()
{
// todo
}
// Or a combination of the above
#[Query(type: new UnionType('FoobarUnionType'))]
public function getFoobar() : UserType|OtherType
{
// todo
}
}
GraphQL Attribute Schema provides built-in pagination following the Relay Connection specification.
For more details, check out:
use Jerowork\GraphqlAttributeSchema\Attribute\Cursor;
use Jerowork\GraphqlAttributeSchema\Attribute\Field;
use Jerowork\GraphqlAttributeSchema\Attribute\Option\ConnectionType;
use Jerowork\GraphqlAttributeSchema\Attribute\Query;
use Jerowork\GraphqlAttributeSchema\Attribute\Type;
use Jerowork\GraphqlAttributeSchema\Type\Connection\Connection;
use Jerowork\GraphqlAttributeSchema\Type\Connection\EdgeArgs;
final readonly class UsersQuery
{
#[Query(type: new ConnectionType(User::class))]
public function getUsers(string $status, EdgeArgs $edgeArgs) : Connection
{
// Retrieve a slice of users based on EdgeArgs and filters like status
// ...
return new Connection($users);
}
}
#[Type]
final readonly class User
{
public function __construct(
#[Field]
#[Cursor]
string $id,
// ... other fields
) {}
}
With this setup, you can query paginated users like this:
query {
users(status: "active", first: 15) {
edges {
cursor
node {
id
# ... other fields
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
To set up a connection, use ConnectionType
:
#[Query]
and #[Mutation]
.#[Type]
methods.If you use ConnectionType
, the return type must be Connection
.
This is a DTO that contains a list of entities (nodes) and pagination details:
hasPreviousPage
hasNextPage
startCursor
endCursor
For pagination input, use EdgeArgs:
first
, last
; number of records to fetch.after
, before
; cursors for slicing.You can also define additional custom input arguments if needed.
Each ‘node’ in a connection must have a #[Cursor]
attribute. You can add this on a property or a method. This defines the cursor output for each “edge.”
💡 Note: If you don’t define a #[Cursor]
, the cursor will always be null
when querying.