ivome/graphql-relay-php
PHP library for adding Relay support to GraphQL servers. Provides helpers for cursor-based connections, edges, page info, global object identification, and Relay-style mutations, making it easier to build spec-compliant GraphQL APIs.
composer require ivome/graphql-relay-php. It is a companion to webonyx/graphql-php (historically ^14.0), so check your graphql-php version before pulling it in — this is the first thing that will bite you.GraphQLRelay\Relay. The first files worth reading in the repo are src/Relay.php, src/Connection/Connection.php, src/Node/Node.php, src/Mutation/Mutation.php — one cluster per Relay concept.graphql-relay-js. If a helper feels under-documented, the JS reference implementation's docs map almost one-to-one (connectionFromArray, globalIdField, mutationWithClientMutationId, nodeDefinitions).User, Post, whatever) and turn one of its list fields into a Relay connection. A ~30-line change exercises the whole toolchain: connection definitions, cursor encoding, and pageInfo.Rough shape:
use GraphQLRelay\Relay;
$userType = new ObjectType([
'name' => 'User',
'fields' => [
'id' => Relay::globalIdField('User', fn($u) => $u['id']),
'name' => ['type' => Type::string()],
],
]);
$userConnection = Relay::connectionDefinitions([
'name' => 'User',
'nodeType' => $userType,
]);
// 'users' field then looks like:
// 'type' => $userConnection['connectionType'],
// 'args' => Relay::connectionArgs(),
// 'resolve' => fn($root, $args) => Relay::connectionFromArray($users, $args),
Then print the schema (GraphQL\Utils\SchemaPrinter::doPrint($schema)) and confirm you now have edges { node, cursor }, pageInfo { hasNextPage, hasPreviousPage, startCursor, endCursor }. If Relay's client compiler is happy with that output, your foundation is correct.
Connections and pagination
Relay::connectionDefinitions([...]) and reuse ['connectionType'] everywhere that list is exposed. Don't hand-roll edges/pageInfo; Relay's compiler hard-codes those field names and their nullability.Relay::connectionFromArray($array, $args). For anything database-backed: Relay::connectionFromArraySlice($slice, $args, ['sliceStart' => $offset, 'arrayLength' => $total]).first/after into LIMIT first + 1 OFFSET cursorToOffset($after). Feed the resulting page into connectionFromArraySlice so the helper can compute hasNextPage/hasPreviousPage and edge cursors for you.Node and global IDs
Relay::nodeDefinitions($idFetcher, $typeResolver) once at schema boot. The $idFetcher receives the opaque global ID; call Relay::fromGlobalId($id) inside to get ['type' => ..., 'id' => ...], then dispatch on the type.$typeResolver receives the object your fetcher returned and maps it to the corresponding object type. Keeping fetch and type-resolution in one place is what keeps node(id:) consistent with your typed fields.Relay::globalIdField('User', $getter) for every id field. Mixing raw integer IDs and global IDs on the same schema is a reliable way to get silent cache corruption on the client — refs stop resolving and updates disappear.Mutations
Relay::mutationWithClientMutationId([...]) over hand-written mutation fields. It wraps your payload inside input { ... } and echoes clientMutationId, which is what makes Relay's optimistic updates and mutation tracking work.mutateAndGetPayload performs the write and returns a plain payload array; outputFields re-exposes each field with a resolver that reads from that payload. Keep input and output field lists small and strongly typed so the client compiler can validate them.clientMutationId will make the mutation appear to do nothing on the client even though the server succeeded.Schema assembly
Schema object. You still assemble GraphQL\Type\Schema yourself, so the library drops cleanly into an existing schema, type registry, or middleware stack.Relay::pluralIdentifyingRootField(...) rather than issuing N separate node(id:) round-trips.Day-to-day workflow with relay-compiler
.graphql file and run relay-compiler against it. Misused helpers (missing pageInfo, wrong cursor nullability, wrong arg types) surface as compiler errors instead of runtime nulls.webonyx/graphql-php 14.x. On graphql-php 15+ you'll likely hit type-hint and config-shape mismatches around FieldDefinition/EnumType construction. Either pin graphql-php or fork the three or four helper files you actually use — the library is small enough that vendoring is viable.Relay::offsetToCursor(3) is literally base64("arrayconnection:3"). Insert or delete a row and every subsequent cursor shifts. Never persist them; never compare them across requests. If you need stable cursors, write your own encoder and skip the connection helpers for that field.sliceStart must be absolute. connectionFromArraySlice wants the offset of the first element within the full list, not 0 per page. Passing 0 is the classic bug that makes hasPreviousPage lie and restarts cursors on page 2.hasNextPage off-by-one. When feeding from SQL, fetch $first + 1 rows and set arrayLength to the total; the helper handles the edge case. connectionFromArray avoids the issue entirely but requires the whole list in memory.toGlobalId('User', 1) embeds the string "User" into every ID the client has cached. Renaming a type is a breaking change for existing clients — treat it like a database migration.nodeDefinitions returns an interface plus field configs; you wire them in yourself. Forgetting to add the node field to your query type is the most common "why isn't refetch working" bug — nothing is auto-registered.SchemaPrinter::doPrint($schema) and a round-trip through GraphQL\Utils\BuildSchema will expose almost everything. Diff the printed schema against what relay-compiler generated before suspecting the client.MyRelay::connection(...)) to add stable cursors, DTO support, or caching is easy. nodeDefinitions's $idFetcher is the natural place to add dataloader-style batching, since every node(id:) call funnels through it.mutateAndGetPayload and $idFetcher. Re-deriving authorization inside each resolver is exactly where Relay-style ID fetching tends to get it wrong.How can I help you explore Laravel packages today?