{"id":23152981,"url":"https://github.com/t3n/graphql","last_synced_at":"2025-07-18T22:35:07.654Z","repository":{"id":33575656,"uuid":"159184163","full_name":"t3n/graphql","owner":"t3n","description":"Flow Package to add graphql APIs to Neos and Flow that also supports some advanced features ","archived":false,"fork":false,"pushed_at":"2025-01-21T15:11:34.000Z","size":111,"stargazers_count":16,"open_issues_count":5,"forks_count":9,"subscribers_count":10,"default_branch":"master","last_synced_at":"2025-06-21T21:02:50.542Z","etag":null,"topics":["flowframework","flowpackage","graphql","neoscms","php"],"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/t3n.png","metadata":{"files":{"readme":"Readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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}},"created_at":"2018-11-26T14:41:06.000Z","updated_at":"2025-04-09T10:13:36.000Z","dependencies_parsed_at":"2025-02-21T20:11:18.342Z","dependency_job_id":"ae01e99a-49d5-4de8-9fa6-402c91a4834e","html_url":"https://github.com/t3n/graphql","commit_stats":{"total_commits":62,"total_committers":8,"mean_commits":7.75,"dds":0.3709677419354839,"last_synced_commit":"f8aafc967eec7820395145e33152ded1071cddec"},"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"purl":"pkg:github/t3n/graphql","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/t3n%2Fgraphql","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/t3n%2Fgraphql/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/t3n%2Fgraphql/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/t3n%2Fgraphql/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/t3n","download_url":"https://codeload.github.com/t3n/graphql/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/t3n%2Fgraphql/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265846883,"owners_count":23838160,"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","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":["flowframework","flowpackage","graphql","neoscms","php"],"created_at":"2024-12-17T19:20:03.525Z","updated_at":"2025-07-18T22:35:07.605Z","avatar_url":"https://github.com/t3n.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![CircleCI](https://circleci.com/gh/t3n/graphql.svg?style=svg)](https://circleci.com/gh/t3n/graphql) [![Latest Stable Version](https://poser.pugx.org/t3n/graphql/v/stable)](https://packagist.org/packages/t3n/graphql) [![Total Downloads](https://poser.pugx.org/t3n/graphql/downloads)](https://packagist.org/packages/t3n/graphql)\n\n# t3n.GraphQL\n\nFlow Package to add graphql APIs to [Neos and Flow](https://neos.io) that also supports advanced features like schema stitching, validation rules, schema directives and more.\nThis package doesn't provide a GraphQL client to test your API. We suggest to use the [GraphlQL Playground](https://github.com/prisma/graphql-playground)\n\nSimply install the package via composer:\n\n```bash\ncomposer require \"t3n/graphql\"\n```\n\nVersion 2.x supports neos/flow \u003e= 6.0.0\n\n## Configuration\n\nIn order to use your GraphQL API endpoint some configuration is necessary.\n\n### Endpoints\n\nLet's assume that the API should be accessible under the URL http://localhost/api/my-endpoint.\n\nTo make this possible, you first have to add the route to your `Routes.yaml`:\n\n```yaml\n- name: 'GraphQL API'\n  uriPattern: 'api/\u003cGraphQLSubroutes\u003e'\n  subRoutes:\n    'GraphQLSubroutes':\n      package: 't3n.GraphQL'\n      variables:\n        'endpoint': 'my-endpoint'\n```\n\nDon't forget to load your routes at all:\n\n```yaml\nNeos:\n  Flow:\n    mvc:\n      routes:\n        'Your.Package':\n          position: 'start'\n```\n\nNow the route is activated and available.\n\n### Schema\n\nThe next step is to define a schema that can be queried.\n\nCreate a `schema.graphql` file:\n\n/Your.Package/Resources/Private/GraphQL/schema.root.graphql\n\n```graphql schema\ntype Query {\n  ping: String!\n}\n\ntype Mutation {\n  pong: String!\n}\n\nschema {\n  query: Query\n  mutation: Mutation\n}\n```\n\nUnder the hood we use [t3n/graphql-tools](https://github.com/t3n/graphql-tools). This package is a php port from\n[Apollos graphql-tools](https://github.com/apollographql/graphql-tools/). This enables you to use some advanced\nfeatures like schema stitching. So it's possible to configure multiple schemas per endpoint. All schemas\nwill be merged internally together to a single schema.\n\nAdd a schema to your endpoint like this:\n\n```yaml\nt3n:\n  GraphQL:\n    endpoints:\n      'my-endpoint': # use your endpoint variable here\n        schemas:\n          root: # use any key you like here\n            typeDefs: 'resource://Your.Package/Private/GraphQL/schema.root.graphql'\n```\n\nTo add another schema just add a new entry below the `schemas` index for your endpoint.\n\nYou can also use the extend feature:\n\n/Your.Package/Resources/Private/GraphQL/schema.yeah.graphql\n\n```graphql schema\nextend type Query {\n  yippie: String!\n}\n```\n\n```yaml\nt3n:\n  GraphQL:\n    endpoints:\n      'my-endpoint': #\n        schemas:\n          yeah:\n            typeDefs: 'resource://Your.Package/Private/GraphQL/schema.yeah.graphql'\n```\n\n### Resolver\n\nNow you need to add some Resolver. You can add a Resolver for each of your types.\nGiven this schema:\n\n```graphql schema\ntype Query {\n  product(id: ID!): Product\n  products: [Product]\n}\n\ntype Product {\n  id: ID!\n  name: String!\n  price: Float!\n}\n```\n\nYou might want to configure Resolver for both types:\n\n```yaml\nt3n:\n  GraphQL:\n    endpoints:\n      'my-endpoint':\n        schemas:\n          mySchema:\n            resolvers:\n              Query: 'Your\\Package\\GraphQL\\Resolver\\QueryResolver'\n              Product: 'Your\\Package\\GraphQL\\Resolver\\ProductResolver'\n```\n\nEach resolver must implement `t3n\\GraphQL\\ResolverInterface` !\n\nYou can also add resolvers dynamically so you don't have to configure each resolver separately:\n\n```yaml\nt3n:\n  GraphQL:\n    endpoints:\n      'my-endpoint':\n        schemas:\n          mySchema:\n            resolverPathPattern: 'Your\\Package\\GraphQL\\Resolver\\Type\\{Type}Resolver'\n            resolvers:\n              Query: 'Your\\Package\\GraphQL\\Resolver\\QueryResolver'\n```\n\nWith this configuration the class `Your\\Package\\GraphQL\\Resolver\\Type\\ProductResolver` would be responsible\nfor queries on a Product type. The {Type} will evaluate to your type name.\n\nAs a third option you can create resolvers programmatically. Therefore you can register a class that implements\nthe `t3n\\GraphQL\\ResolverGeneratorInterface`. This might be useful to auto generate a resolver mapping:\n\n```yaml\nt3n:\n  GraphQL:\n    endpoints:\n      'my-endpoint':\n        schemas:\n          mySchema:\n            resolverGenerator: 'Your\\Package\\GraphQL\\Resolver\\ResolverGenerator'\n```\n\nThe Generator must return an array with this structure: ['typeName' =\u003e \\Resolver\\Class\\Name]\n\n☝️ Note: Your Resolver can override each other. All resolver configurations are applied in this order:\n- ResolverGenerator\n- dynamic \"{Type}Resolver\"\n- specific Resolver\n\n#### Resolver Implementation\n\nA implementation for our example could look like this (pseudocode):\n\n```php\n\u003c?php\n\nnamespace Your\\Package\\GraphQL\\Resolver;\n\nuse Neos\\Flow\\Annotations as Flow;\nuse t3n\\GraphQL\\ResolverInterface;\n\nclass QueryResolver implements ResolverInterface\n{\n    protected $someServiceToFetchProducts;\n\n    public function products($_, $variables): array\n    {\n        // return an array with products\n        return $this-\u003esomeServiceToFetchProducts-\u003efindAll();\n    }\n\n    public function product($_, $variables): ?Product\n    {\n        $id = $variables['id'];\n        return $this-\u003esomeServiceToFetchProducts-\u003egetProductById($id);\n    }\n}\n```\n\n```php\n\u003c?php\n\nnamespace Your\\Package\\GraphQL\\Resolver\\Type;\n\nuse Neos\\Flow\\Annotations as Flow;\nuse t3n\\GraphQL\\ResolverInterface;\n\nclass ProductResolver implements ResolverInterface\n{\n    public function name(Product $product): array\n    {\n        // this is just an overload example\n        return $product-\u003egetName();\n    }\n}\n```\n\nAn example query like:\n\n```graphql\nquery {\n  products {\n    id\n    name\n    price\n  }\n}\n```\n\nwould invoke the QueryResolver in first place and call the `products()` method. This method\nreturns an array with Product objects. For each of the objects the ProductResolver is used.\nTo fetch the actual value there is a DefaultFieldResolver. If you do not configure a method\nnamed as the requests property it will be used to fetch the value. The DefaultFieldResolver\nwill try to fetch the data itself via `ObjectAccess::getProperty($source, $fieldName)`.\nSo if your Product Object has a `getName()` it will be used. You can still overload the\nimplementation just like in the example.\n\nAll resolver methods share the same signature:\n\n```php\nmethod($source, $args, $context, $info)\n```\n\n#### Interface Types\n\nWhen working with interfaces, you need Resolvers for your interfaces as well. Given this schema:\n\n```graphql schema\ninterface Person {\n    firstName: String!\n    lastName: String!\n}\n\ntype Customer implements Person {\n    firstName: String!\n    lastName: String!\n    email: String!\n}\n\ntype Supplier implements Person {\n    firstName: String!\n    lastName: String!\n    phone: String\n}\n```\n\nYou need to configure a `Person` Resolver as well as Resolvers for the concrete implementations.\n\nWhile the concrete Type Resolvers do not need any special attention, the `Person` Resolver implements the \n`__resolveType` function returning the type names:\n\n```php\n\u003c?php\n\nnamespace Your\\Package\\GraphQL\\Resolver\\Type;\n\nuse t3n\\GraphQL\\ResolverInterface;\n\nclass PersonResolver implements ResolverInterface\n{\n    public function __resolveType($source): ?string\n    {\n        if ($source instanceof Customer) {\n            return 'Customer';\n        } elseif ($source instanceof Supplier) {\n            return 'Supplier';\n        }\n        return null;\n    }\n}\n```\n\n#### Union Types\n\nUnions are resolved the same way as interfaces. For the example above, the corresponding Schema looks like this:\n\n```graphql schema\nunion Person = Customer | Supplier\n```\n\nIt is resolved again with a `PersonResolver` implementing the `__resolveType` function.\n\n#### Enum Types\n\nEnums are resolved automatically. The Resolver method simply returns a string matching the Enum value. For the Schema:\n```graphql schema\nenum Status {\n  ACTIVE\n  INACTIVE\n}\n\ntype Customer {\n    status: Status!\n}\n```\n\nThe `CustomerResolver` method returns the value as string:\n```php\npublic function status(Customer $customer): string\n{\n    return $customer-\u003eisActive() ? 'ACTIVE' : 'INACTIVE';\n}\n```\n\n#### Scalar Types\n\nTypes at the leaves of a Json tree are defined by Scalars. Own Scalars are implemented by a Resolver \nimplementing the functions `serialize()`, `parseLiteral()` and `parseValue()`.\n\nThis example shows the implementation of a `DateTime` Scalar Type. For the given Schema definition:\n\n```graphql schema\nscalar DateTime\n```\n\nThe `DateTimeResolver` looks the following when working with Unix timestamps:\n\n```php\n\u003c?php\n\nnamespace Your\\Package\\GraphQL\\Resolver\\Type;\n\nuse DateTime;\nuse GraphQL\\Language\\AST\\IntValueNode;\nuse GraphQL\\Language\\AST\\Node;\nuse t3n\\GraphQL\\ResolverInterface;\n\nclass DateTimeResolver implements ResolverInterface\n{\n    public function serialize(DateTime $value): ?int\n    {\n        return $value-\u003egetTimestamp();\n    }\n\n    public function parseLiteral(Node $ast): ?DateTime\n    {\n        if ($ast instanceof IntValueNode) {\n            $dateTime = new DateTime();\n            $dateTime-\u003esetTimestamp((int)$ast-\u003evalue);\n            return $dateTime;\n        }\n        return null;\n    }\n\n    public function parseValue(int $value): DateTime\n    {\n        $dateTime = new DateTime();\n        $dateTime-\u003esetTimestamp($value);\n        return $dateTime;\n    }\n}\n```\n\nYou have to make the `DateTimeResolver` available again through one of the configuration options in the Settings.yaml.\n\n### Context\n\nThe third argument in your Resolver method signature is the Context.\nBy Default it's set to `t3n\\GraphQContext` which exposes the current request.\n\nIt's easy to set your very own Context per endpoint. This might be handy to share some Code or Objects\nbetween all your Resolver implementations. Make sure to extend `t3n\\GraphQContext`\n\nLet's say we have an graphql endpoint for a shopping basket (simplified):\n\n```graphql schema\ntype Query {\n  basket: Basket\n}\n\ntype Mutation {\n  addItem(item: BasketItemInput): Basket\n}\n\ntype Basket {\n  items: [BasketItem]\n  amount: Float!\n}\n\ntype BasketItem {\n  id: ID!\n  name: String!\n  price: Float!\n}\n\ninput BasketItemInput {\n  name: String!\n  price: Float!\n}\n```\n\nFirst of all configure your context for your shopping endpoint:\n\n```yaml\nt3n:\n  GraphQL:\n    endpoints:\n      'shop':\n        context: 'Your\\Package\\GraphQL\\ShoppingBasketContext'\n        schemas:\n          basket:\n            typeDefs: 'resource://Your.Package/Private/GraphQL/schema.graphql'\n            resolverPathPattern: 'Your\\Package\\GraphQL\\Resolver\\Type\\{Type}Resolver'\n            resolvers:\n              Query: 'Your\\Package\\GraphQL\\Resolver\\QueryResolver'\n              Mutation: 'Your\\Package\\GraphQL\\Resolver\\MutationResolver'\n```\n\nA context for this scenario would inject the current basket (probably flow session scoped);\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nnamespace Your\\Package\\GraphQL;\n\nuse Neos\\Flow\\Annotations as Flow;\nuse Your\\Package\\Shop\\Basket;\nuse t3n\\GraphQL\\Context as BaseContext;\n\nclass ShoppingBasketContext extends BaseContext\n{\n    /**\n     * @Flow\\Inject\n     *\n     * @var Basket\n     */\n    protected $basket;\n\n    public function getBasket()\n    {\n        return $this-\u003ebasket;\n    }\n}\n```\n\nAnd the corresponding resolver classes:\n\n```php\n\u003c?php\n\nnamespace Your\\Package\\GraphQL\\Resolver;\n\nuse Neos\\Flow\\Annotations as Flow;\nuse t3n\\GraphQL\\ResolverInterface;\n\nclass QueryResolver implements ResolverInterface\n{\n    protected $someServiceToFetchProducts;\n\n    // Note the resolver method signature. The context is available as third param\n    public function basket($_, $variables, ShoppingBasketContext $context): array\n    {\n        return $context-\u003egetBasket();\n    }\n}\n```\n\n```php\n\u003c?php\n\nnamespace Your\\Package\\GraphQL\\Resolver;\n\nuse Neos\\Flow\\Annotations as Flow;\nuse t3n\\GraphQL\\ResolverInterface;\n\nclass MutationResolver implements ResolverInterface\n{\n    protected $someServiceToFetchProducts;\n\n    public function addItem($_, $variables, ShoppingBasketContext $context): Basket\n    {\n        // construct your item with the input (simplified, don't forget validation etc.)\n        $item = new BasketItem();\n        $item-\u003esetName($variables['name']);\n        $item-\u003esetPrice($variables['price']);\n\n        $basket = $context-\u003egetBasket();\n        $basket-\u003eaddItem($item);\n\n        return $basket;\n    }\n}\n```\n\n### Log incoming requests\n\nYou can enable logging of incoming requests per endpoint:\n\n```yaml\nt3n:\n  GraphQL:\n    endpoints:\n      'your-endpoint':\n        logRequests: true\n```\n\nOnce activated all incoming requests will be logged to `Data/Logs/GraphQLRequests.log`. Each log entry\nwill contain the endpoint, query and variables.\n\n### Secure your endpoint\n\nTo secure your api endpoints you have several options. The easiest way is to just configure\nsome privilege for your Resolver:\n\n```yaml\nprivilegeTargets:\n  'Neos\\Flow\\Security\\Authorization\\Privilege\\Method\\MethodPrivilege':\n    'Your.Package:Queries':\n      matcher: 'method(public Your\\Package\\GraphQL\\Resolver\\QueryResolver-\u003e.*())'\n    'Your.Package:Mutations':\n      matcher: 'method(public Your\\Package\\GraphQL\\Resolver\\MutationResolver-\u003e.*())'\n\nroles:\n  'Your.Package:SomeRole':\n    privileges:\n      - privilegeTarget: 'Your.Package:Queries'\n        permission: GRANT\n      - privilegeTarget: 'Your.Package:Mutations'\n        permission: GRANT\n```\n\nYou could also use a custom Context to access the current logged in user.\n\n### Schema directives\n\nBy default this package provides three directives:\n\n- AuthDirective\n- CachedDirective\n- CostDirective\n\nTo enable those Directives add this configuration to your endpoint:\n\n```yaml\nt3n:\n  GraphQL:\n    endpoints:\n      'your-endpoint':\n        schemas:\n          root: # use any key you like here\n            typeDefs: 'resource://t3n.GraphQL/Private/GraphQL/schema.root.graphql'\n        schemaDirectives:\n          auth: 't3n\\GraphQL\\Directive\\AuthDirective'\n          cached: 't3n\\GraphQL\\Directive\\CachedDirective'\n          cost: 't3n\\GraphQL\\Directive\\CostDirective'\n```\n\n#### AuthDirective\n\nThe AuthDirective will check the security context for current authenticated roles.\nThis enables you to protect objects or fields to user with given roles.\n\nUse it like this to allow Editors to update a product but restrict the removal to Admins only:\n\n```graphql schema\ntype Mutation {\n    updateProduct(): Product @auth(required: \"Neos.Neos:Editor\")\n    removeProduct(): Boolean @auth(required: \"Neos.Neos:Administrator\")\n}\n```\n\n#### CachedDirective\n\nCaching is always a thing. Some queries might be expensive to resolve and it's worthy to cache the result.\nTherefore you should use the CachedDirective:\n\n```graphql schema\ntype Query {\n  getProduct(id: ID!): Product @cached(maxAge: 100, tags: [\"some-tag\", \"another-tag\"])\n}\n```\n\nThe CachedDirective will use a flow cache `t3n_GraphQL_Resolve` as a backend. The directive accepts a maxAge\nargument as well as tags. Check the flow documentation about caching to learn about them!\nThe cache entry identifier will respect all arguments (id in this example) as well as the query path.\n\n#### CostDirective\n\nThe CostDirective will add a complexity function to your fields and objects which is used by some validation rules.\nEach type and children has a default complexity of 1.\nIt allows you to annotate cost values and multipliers just like this:\n\n```graphql schema\ntype Product @cost(complexity: 5) {\n  name: String! @cost(complexity: 3)\n  price: Float!\n}\n\ntype Query {\n  products(limit: Int!): [Product!]! @cost(multipliers: [\"limit\"])\n}\n```\n\nIf you query `produts(limit: 3) { name, price }` the query would have a cost of:\n\n9 per product (5 for the product itself and 3 for fetching the name and 1 for the price (default complexity)) multiplied with 3\ncause we defined the limit value as an multiplier. So the query would have a total complexity of 27.\n\n### Validation rules\n\nThere are several Validation rules you can enable per endpoint. The most common are the QueryDepth as well as the QueryComplexity\nrule. Configure your endpoint to enable those rules:\n\n```yaml\nt3n:\n  GraphQL:\n    endpoints:\n      'some-endpoint':\n        validationRules:\n          depth:\n            className: 'GraphQL\\Validator\\Rules\\QueryDepth'\n            arguments:\n              maxDepth: 11\n          complexity:\n            className: 'GraphQL\\Validator\\Rules\\QueryComplexity'\n            arguments:\n              maxQueryComplexity: 1000\n```\n\nThe `maxQueryComplexitiy` is calculated via the CostDirective.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ft3n%2Fgraphql","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ft3n%2Fgraphql","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ft3n%2Fgraphql/lists"}