{"id":31721356,"url":"https://github.com/ecodev/graphql-doctrine","last_synced_at":"2025-10-09T03:48:20.589Z","repository":{"id":23475469,"uuid":"99137739","full_name":"Ecodev/graphql-doctrine","owner":"Ecodev","description":"Automatic GraphQL types from Doctrine entities","archived":false,"fork":false,"pushed_at":"2025-07-28T08:42:05.000Z","size":901,"stargazers_count":101,"open_issues_count":5,"forks_count":14,"subscribers_count":14,"default_branch":"master","last_synced_at":"2025-10-06T19:21:05.840Z","etag":null,"topics":["doctrine","doctrine-orm","graphql","graphql-php","hacktoberfest"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Ecodev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2017-08-02T16:19:36.000Z","updated_at":"2025-09-03T13:46:57.000Z","dependencies_parsed_at":"2023-02-17T05:45:47.002Z","dependency_job_id":"993f0163-fde2-42f1-ae58-1e63a9fce78a","html_url":"https://github.com/Ecodev/graphql-doctrine","commit_stats":{"total_commits":180,"total_committers":6,"mean_commits":30.0,"dds":"0.033333333333333326","last_synced_commit":"05fc6187f6912c44577a29ace30050d819a66e7f"},"previous_names":[],"tags_count":39,"template":false,"template_full_name":null,"purl":"pkg:github/Ecodev/graphql-doctrine","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ecodev%2Fgraphql-doctrine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ecodev%2Fgraphql-doctrine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ecodev%2Fgraphql-doctrine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ecodev%2Fgraphql-doctrine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Ecodev","download_url":"https://codeload.github.com/Ecodev/graphql-doctrine/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ecodev%2Fgraphql-doctrine/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279000703,"owners_count":26082911,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-09T02:00:07.460Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["doctrine","doctrine-orm","graphql","graphql-php","hacktoberfest"],"created_at":"2025-10-09T03:48:16.321Z","updated_at":"2025-10-09T03:48:20.580Z","avatar_url":"https://github.com/Ecodev.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# GraphQL Doctrine\n\n[![Build Status](https://github.com/ecodev/graphql-doctrine/workflows/main/badge.svg)](https://github.com/ecodev/graphql-doctrine/actions)\n[![Code Quality](https://scrutinizer-ci.com/g/Ecodev/graphql-doctrine/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Ecodev/graphql-doctrine/?branch=master)\n[![Code Coverage](https://scrutinizer-ci.com/g/Ecodev/graphql-doctrine/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/Ecodev/graphql-doctrine/?branch=master)\n[![Total Downloads](https://poser.pugx.org/ecodev/graphql-doctrine/downloads.png)](https://packagist.org/packages/ecodev/graphql-doctrine)\n[![Latest Stable Version](https://poser.pugx.org/ecodev/graphql-doctrine/v/stable.png)](https://packagist.org/packages/ecodev/graphql-doctrine)\n[![License](https://poser.pugx.org/ecodev/graphql-doctrine/license.png)](https://packagist.org/packages/ecodev/graphql-doctrine)\n[![Join the chat at https://gitter.im/Ecodev/graphql-doctrine](https://badges.gitter.im/Ecodev/graphql-doctrine.svg)](https://gitter.im/Ecodev/graphql-doctrine)\n\nA library to declare GraphQL types from Doctrine entities, PHP type hinting,\nand attributes, and to be used with [webonyx/graphql-php](https://github.com/webonyx/graphql-php).\n\nIt reads most information from type hints, complete some things from existing\nDoctrine attributes and allow further customizations with specialized attributes.\nIt will then create [`ObjectType`](https://webonyx.github.io/graphql-php/type-system/object-types/#object-type-definition) and\n [`InputObjectType`](https://webonyx.github.io/graphql-php/type-system/input-types/#input-object-type)\ninstances with fields for all getter and setter respectively found on Doctrine entities.\n\nIt will **not** build the entire schema. It is up to the user to use automated\ntypes, and other custom types, to define root queries.\n\n## Quick start\n\nInstall the library via composer:\n\n```sh\ncomposer require ecodev/graphql-doctrine\n```\n\nAnd start using it:\n\n```php\n\u003c?php\n\nuse GraphQLTests\\Doctrine\\Blog\\Model\\Post;\nuse GraphQLTests\\Doctrine\\Blog\\Types\\DateTimeType;\nuse GraphQLTests\\Doctrine\\Blog\\Types\\PostStatusType;\nuse GraphQL\\Type\\Definition\\ObjectType;\nuse GraphQL\\Type\\Definition\\Type;\nuse GraphQL\\Type\\Schema;\nuse GraphQL\\Doctrine\\DefaultFieldResolver;\nuse GraphQL\\Doctrine\\Types;\nuse Laminas\\ServiceManager\\ServiceManager;\n\n// Define custom types with a PSR-11 container\n$customTypes = new ServiceManager([\n    'invokables' =\u003e [\n        DateTimeImmutable::class =\u003e DateTimeType::class,\n        'PostStatus' =\u003e PostStatusType::class,\n    ],\n    'aliases' =\u003e [\n        'datetime_immutable' =\u003e DateTimeImmutable::class, // Declare alias for Doctrine type to be used for filters\n    ],\n]);\n\n// Configure the type registry\n$types = new Types($entityManager, $customTypes);\n\n// Configure default field resolver to be able to use getters\nGraphQL::setDefaultFieldResolver(new DefaultFieldResolver());\n\n// Build your Schema\n$schema = new Schema([\n    'query' =\u003e new ObjectType([\n        'name' =\u003e 'query',\n        'fields' =\u003e [\n            'posts' =\u003e [\n                'type' =\u003e Type::listOf($types-\u003egetOutput(Post::class)), // Use automated ObjectType for output\n                'args' =\u003e [\n                    [\n                        'name' =\u003e 'filter',\n                        'type' =\u003e $types-\u003egetFilter(Post::class), // Use automated filtering options\n                    ],\n                    [\n                        'name' =\u003e 'sorting',\n                        'type' =\u003e $types-\u003egetSorting(Post::class), // Use automated sorting options\n                    ],\n                ],\n                'resolve' =\u003e function ($root, $args) use ($types): void {\n                    $queryBuilder = $types-\u003ecreateFilteredQueryBuilder(Post::class, $args['filter'] ?? [], $args['sorting'] ?? []);\n\n                    // execute query...\n                },\n            ],\n        ],\n    ]),\n    'mutation' =\u003e new ObjectType([\n        'name' =\u003e 'mutation',\n        'fields' =\u003e [\n            'createPost' =\u003e [\n                'type' =\u003e Type::nonNull($types-\u003egetOutput(Post::class)),\n                'args' =\u003e [\n                    'input' =\u003e Type::nonNull($types-\u003egetInput(Post::class)), // Use automated InputObjectType for input\n                ],\n                'resolve' =\u003e function ($root, $args): void {\n                    // create new post and flush...\n                },\n            ],\n            'updatePost' =\u003e [\n                'type' =\u003e Type::nonNull($types-\u003egetOutput(Post::class)),\n                'args' =\u003e [\n                    'id' =\u003e Type::nonNull(Type::id()), // Use standard API when needed\n                    'input' =\u003e $types-\u003egetPartialInput(Post::class),  // Use automated InputObjectType for partial input for updates\n                ],\n                'resolve' =\u003e function ($root, $args): void {\n                    // update existing post and flush...\n                },\n            ],\n        ],\n    ]),\n]);\n```\n\n## Usage\n\nThe public API is limited to the public methods on `TypesInterface`, `Types`'s constructor, and the attributes.\n\nHere is a quick overview of `TypesInterface`:\n\n- `$types-\u003eget()` to get custom types\n- `$types-\u003egetOutput()` to get an `ObjectType` to be used in queries\n- `$types-\u003egetFilter()` to get an `InputObjectType` to be used in queries\n- `$types-\u003egetSorting()` to get an `InputObjectType` to be used in queries\n- `$types-\u003egetInput()` to get an `InputObjectType` to be used in mutations (typically for creation)\n- `$types-\u003egetPartialInput()` to get an `InputObjectType` to be used in mutations (typically for update)\n- `$types-\u003egetId()` to get an `EntityIDType` which may be used to receive an\n  object from database instead of a scalar\n- `$types-\u003ehas()` to check whether a type exists\n- `$types-\u003ecreateFilteredQueryBuilder()` to be used in query resolvers\n\n### Information priority\n\nTo avoid code duplication as much as possible, information are gathered from\nseveral places, where available. And each of those might be overridden. The order\nof priority, from the least to most important is:\n\n1. Type hinting\n2. Doc blocks\n3. Attributes\n\nThat means it is always possible to override everything with attributes. But\nexisting type hints and dock blocks should cover the majority of cases.\n\n### Exclude sensitive things\n\nAll getters, and setters, are included by default in the type. And all properties are included in the filters.\nBut it can be specified otherwise for each method and property.\n\nTo exclude a sensitive field from ever being exposed through the API, use `#[API\\Exclude]`:\n\n```php\nuse GraphQL\\Doctrine\\Attribute as API;\n\n/**\n * Returns the hashed password\n *\n * @return string\n */\n#[API\\Exclude]\npublic function getPassword(): string\n{\n    return $this-\u003epassword;\n}\n```\n\nAnd to exclude a property from being exposed as a filter:\n\n```php\nuse GraphQL\\Doctrine\\Attribute as API;\n\n#[ORM\\Column(name: 'password', type: 'string', length: 255)]\n#[API\\Exclude]\nprivate string $password = '';\n```\n\n### Override output types\n\nEven if a getter returns a PHP scalar type, such as `string`, it might be preferable\nto override the type with a custom GraphQL type. This is typically useful for enum\nor other validation purposes, such as email address. This is done by specifying the\nGraphQL type FQCN via `#[API\\Field]` attribute:\n\n```php\nuse GraphQL\\Doctrine\\Attribute as API;\nuse GraphQLTests\\Doctrine\\Blog\\Types\\PostStatusType;\n\n/**\n * Get status\n *\n * @return string\n */\n#[API\\Field(type: PostStatusType::class)]\npublic function getStatus(): string\n{\n    return $this-\u003estatus;\n}\n```\n\n### Type syntax\n\nIn most cases, the type must use the `::class` notation to specify the PHP class that is either implementing the GraphQL\ntype or the entity itself (see [limitations](#limitations)). Use string literals only if you must define it as nullable\nand/or as an array. Never use the short name of an entity (it is only possible for user-defined custom types).\n\nSupported syntaxes (PHP style or GraphQL style) are:\n\n- `MyType::class`\n- `'?Application\\MyType'`\n- `'null|Application\\MyType'`\n- `'Application\\MyType|null'`\n- `'Application\\MyType[]'`\n- `'?Application\\MyType[]'`\n- `'null|Application\\MyType[]'`\n- `'Application\\MyType[]|null'`\n- `'Collection\u003cint, Application\\MyType\u003e'`\n\nThis attribute can be used to override other things, such as `name`, `description`\nand `args`.\n\n### Override arguments\n\nSimilarly to `#[API\\Field]`, `#[API\\Argument]` allows to override the type of argument\nif the PHP type hint is not enough:\n\n```php\nuse GraphQL\\Doctrine\\Attribute as API;\n\n/**\n * Returns all posts of the specified status\n *\n * @param string $status the status of posts as defined in \\GraphQLTests\\Doctrine\\Blog\\Model\\Post\n *\n * @return Collection\u003cint, Post\u003e\n */\npublic function getPosts(\n     #[API\\Argument(type: '?GraphQLTests\\Doctrine\\Blog\\Types\\PostStatusType')]\n    ?string $status = Post::STATUS_PUBLIC\n): Collection\n{\n    // ...\n}\n```\n\nOnce again, it also allows to override other things such as `name`, `description`\nand `defaultValue`.\n\n### Override input types\n\n`#[API\\Input]` is the opposite of `#[API\\Field]` and can be used to override things for\ninput types (setters), typically for validations purpose. This would look like:\n\n```php\nuse GraphQL\\Doctrine\\Attribute as API;\nuse GraphQLTests\\Doctrine\\Blog\\Types\\PostStatusType;\n\n/**\n * Set status\n *\n * @param string $status\n */\n#[API\\Input(type: PostStatusType::class)]\npublic function setStatus(string $status = self::STATUS_PUBLIC): void\n{\n    $this-\u003estatus = $status;\n}\n```\n\nThis attribute also supports `description`, and `defaultValue`.\n\n### Override filter types\n\n`#[API\\FilterGroupCondition]` is the equivalent for filters that are generated from properties.\nSo usage would be like:\n\n```php\nuse GraphQL\\Doctrine\\Attribute as API;\n\n#[API\\FilterGroupCondition(type: '?GraphQLTests\\Doctrine\\Blog\\Types\\PostStatusType')]\n#[ORM\\Column(type: 'string', options: ['default' =\u003e self::STATUS_PRIVATE])]\nprivate string $status = self::STATUS_PRIVATE;\n```\n\nAn important thing to note is that the value of the type specified will be directly used in DQL. That means\nthat if the value is not a PHP scalar, then it must be convertible to string via `__toString()`, or you have to\ndo the conversion yourself before passing the filter values to `Types::createFilteredQueryBuilder()`.\n\n### Custom types\n\nBy default, all PHP scalar types and Doctrine collection are automatically detected\nand mapped to a GraphQL type. However, if some getter return custom types, such\nas `DateTimeImmutable`, or a custom class, then it will have to be configured beforehand.\n\nThe configuration is done with a [PSR-11 container](https://www.php-fig.org/psr/psr-11/)\nimplementation configured according to your needs. In the following example, we use\n[laminas/laminas-servicemanager](https://github.com/laminas/laminas-servicemanager),\nbecause it offers useful concepts such as: invokables, aliases, factories and abstract\nfactories. But any other PSR-11 container implementation could be used instead.\n\nThe keys should be the whatever you use to refer to the type in your model. Typically,\nthat would be either the FQCN of a PHP class \"native\" type such as `DateTimeImmutable`, or the\nFQCN of a PHP class implementing the GraphQL type, or directly the GraphQL type name:\n\n```php\n$customTypes = new ServiceManager([\n    'invokables' =\u003e [\n        DateTimeImmutable::class =\u003e DateTimeType::class,\n        'PostStatus' =\u003e PostStatusType::class,\n    ],\n]);\n\n$types = new Types($entityManager, $customTypes);\n\n// Build schema...\n```\n\nThat way it is not necessary to annotate every single getter returning one of the\nconfigured type. It will be mapped automatically.\n\n### Entities as input arguments\n\nIf a getter takes an entity as parameter, then a specialized `InputType` will\nbe created automatically to accept an `ID`. The entity will then be automatically\nfetched from the database and forwarded to the getter. So this will work out of\nthe box:\n\n```php\npublic function isAllowedEditing(User $user): bool\n{\n    return $this-\u003egetUser() === $user;\n}\n```\n\nYou may also get an input type for an entity by using `Types::getId()` to write\nthings like:\n\n```php\n[\n    // ...\n    'args' =\u003e [\n        'id' =\u003e $types-\u003egetId(Post::class),\n    ],\n    'resolve' =\u003e function ($root, array $args) {\n        $post = $args['id']-\u003egetEntity();\n\n        // ...\n    },\n]\n```\n\n### Partial inputs\n\nIn addition to normal input types, it is possible to get a partial input type via\n`getPartialInput()`. This is especially useful for mutations that update existing\nentities, when we do not want to have to re-submit all fields. By using a partial\ninput, the API client is able to submit only the fields that need to be updated\nand nothing more.\n\nThis potentially reduces network traffic, because the client does not need\nto fetch all fields just to be able re-submit them when he wants to modify only\none field.\n\nAnd it also enables to easily design mass editing mutations where the client would\nsubmit only a few fields to be updated for many entities at once. This could look like:\n\n```php\n\u003c?php\n\n$mutations = [\n    'updatePosts' =\u003e [\n        'type' =\u003e Type::nonNull(Type::listOf(Type::nonNull($types-\u003eget(Post::class)))),\n        'args' =\u003e [\n            'ids' =\u003e Type::nonNull(Type::listOf(Type::nonNull(Type::id()))),\n            'input' =\u003e $types-\u003egetPartialInput(Post::class),  // Use automated InputObjectType for partial input for updates\n        ],\n        'resolve' =\u003e function ($root, $args) {\n            // update existing posts and flush...\n        }\n    ],\n];\n```\n\n### Default values\n\nDefault values are automatically detected from arguments for getters, as seen in\n`getPosts()` example above.\n\nFor setters, the default value will be looked up on the mapped property, if there is\none matching the setter name. But if the setter itself has an argument with a default\nvalue, it will take precedence.\n\nSo the following will make an input type with an optional field `name` with a\ndefault value `john`, an optional field `foo` with a default value `defaultFoo` and\na mandatory field `bar` without any default value:\n\n```php\n#[ORM\\Column(type: 'string']\nprivate $name = 'jane';\n\npublic function setName(string $name = 'john'): void\n{\n    $this-\u003ename = $name;\n}\n\npublic function setFoo(string $foo = 'defaultFoo'): void\n{\n    // do something\n}\n\npublic function setBar(string $bar): void\n{\n    // do something\n}\n```\n\n### Filtering and sorting\n\nIt is possible to expose generic filtering for entity fields and their types to let users easily\ncreate and apply generic filters. This expose basic SQL-like syntax that should cover most simple\ncases.\n\nFilters are structured in an ordered list of groups. Each group contains an unordered set of joins\nand conditions on fields. For simple cases a single group of a few conditions would probably be enough.\nBut the ordered list of group allow more advanced filtering with `OR` logic between a set of conditions.\n\nIn the case of the `Post` class, it would generate [that GraphQL schema](tests/data/PostFilter.graphqls)\nfor filtering, and for sorting it would be [that simpler schema](tests/data/PostSorting.graphqls).\n\nFor concrete examples of possibilities and variables syntax, refer to the\n[test cases](tests/data/query-builder).\n\nFor security and complexity reasons, it is not meant to solve advanced use cases. For those it is\npossible to write custom filters and sorting.\n\n#### Custom filters\n\nA custom filer must extend `AbstractOperator`. This will allow to define custom arguments for\nthe API, and then a method to build the DQL condition corresponding to the argument.\n\nThis would also allow to filter on joined relations by carefully adding joins when necessary.\n\nThen a custom filter might be used like so:\n\n```php\nuse Doctrine\\ORM\\Mapping as ORM;\nuse GraphQL\\Doctrine\\Attribute as API;\nuse GraphQLTests\\Doctrine\\Blog\\Filtering\\SearchOperatorType;\n\n/**\n * A blog post with title and body\n */\n#[ORM\\Entity]\n#[API\\Filter(field: 'custom', operator: SearchOperatorType::class, type: 'string')]\nfinal class Post extends AbstractModel\n```\n\n#### Custom sorting\n\nA custom sorting option must implement `SortingInterface`. The constructor has no arguments and\nthe `__invoke()` must define how to apply the sorting.\n\nSimilarly to custom filters, it may be possible to carefully add joins if necessary.\n\nThen a custom sorting might be used like so:\n\n```php\nuse Doctrine\\ORM\\Mapping as ORM;\nuse GraphQL\\Doctrine\\Attribute as API;\nuse GraphQLTests\\Doctrine\\Blog\\Sorting\\UserName;\n\n/**\n * A blog post with title and body\n */\n#[ORM\\Entity]\n#[API\\Sorting([UserName::class])]\nfinal class Post extends AbstractModel\n```\n\n## Limitations\n\n### Namespaces\n\nThe `use` statement is not supported. So types in attributes or doc blocks must\nbe the FQCN, or the name of a user-defined custom types (but never the short name of an entity).\n\n### Composite identifiers\n\nEntities with composite identifiers are not supported for automatic creation of\ninput types. Possible workarounds are to change input argument to be something\nelse than an entity, write custom input types and use them via attributes, or\nadapt the database schema.\n\n### Logical operators in filtering\n\nLogical operators support only two levels, and second level cannot mix logic operators. In SQL\nthat would mean only one level of parentheses. So you can generate SQL that would look like:\n\n```sql\n-- mixed top level\nWHERE cond1 AND cond2 OR cond3 AND ...\n\n-- mixed top level and non-mixed sublevels\nWHERE cond1 OR (cond2 OR cond3 OR ...) AND (cond4 AND cond5 AND ...) OR ...\n```\n\nBut you **cannot** generate SQL that would like that:\n\n```sql\n-- mixed sublevels does NOT work\nWHERE cond1 AND (cond2 OR cond3 AND cond4) AND ...\n\n-- more than two levels will NOT work\nWHERE cond1 OR (cond2 AND (cond3 OR cond4)) OR ...\n```\n\nThose cases would probably end up being too complex to handle on the client-side. And we recommend\ninstead to implement them as a custom filter on the server side, in order to hide complexity\nfrom the client and benefit from Doctrine's QueryBuilder full flexibility.\n\n### Sorting on join\n\nOut of the box, it is not possible to sort by a field from a joined relation.\nThis should be done via a custom sorting to ensure that joins are done properly.\n\n## Prior work\n\n[Doctrine GraphQL Mapper](https://github.com/rahuljayaraman/doctrine-graphql) has\nbeen an inspiration to write this package. While the goals are similar, the way\nit works is different. In Doctrine GraphQL Mapper, attributes are spread between\nproperties and methods (and classes for filtering), but we work only on methods.\nSetup seems slightly more complex, but might be more flexible. We built on conventions\nand widespread use of PHP type hinting to have an easier out-of-the-box experience.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fecodev%2Fgraphql-doctrine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fecodev%2Fgraphql-doctrine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fecodev%2Fgraphql-doctrine/lists"}