Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[4.10.3] — 2026-10-15
Fixed
- The schema validation checked no entity with the default
mapping.entity.dir,%kernel.project_dir%/src/GraphQL/Entity: the directory was read before%kernel.project_dir%was resolved, so that no entity was found. The metadata cache warmer, given the same list, warmed nothing either.
[4.10.2] — 2026-10-15
4.10.1 is withdrawn: its tag pointed by mistake at the code of 5.0. 4.10.2 holds the fix meant for it, on top of 4.10.0.
Fixed
- Profiler: opening a stored profile with slow queries failed with "Typed property ...::$slowQueryThreshold must not be accessed before initialization": a profile read from the storage only has its data, the threshold is now kept there.
[4.10.0] — 2026-10-15
Added
count()with Data API Builder works on an entity whose identifier is not a number (e.g. astringcode), or which has no identifier: the schema is introspected once, cached invariables.cache_pool, and the entity is counted on its first numeric field never null. Data API Builder only counts the non-null values of a numeric field, and rejectedcount(field: <identifier>)with "The specified argument value does not match the argument type". An entity with no such field gets aLogicExceptiontelling why. Nothing changes, and no introspection is sent, for an identifier typedintorfloat.
Changed
CountingDialect::countField()receivesnullas$identifierFieldwhen the property of the identifier is typed with neitherintnorfloat. The dialects of the bundle other thanDataApiBuilderDialectignore it.
[4.9.2] — 2026-10-15
Fixed
- Profiler: "Copy" and "Copy as cURL" failed on an application served over plain HTTP (other than localhost), where the browser offers no Clipboard API; they now copy through a selected text area there.
[4.9.1] — 2026-10-15
Fixed
- Profiler: the AST of a query with a filter or an ordering failed to render ("Array to string conversion"); its arguments are now shown as JSON.
headers: the dashes of the header names were turned into underscores by the configuration, so thatX-API-Keywas sent asX_API_Key.
[4.9.0] — 2026-10-15
Added
GraphqlManager::getRepository(), as DoctrineEntityManager::getRepository(): the repository service named in therepositoryClassof#[Entity], a generic one otherwise. The repositories extendingGraphqlEntityRepositoryare found by autoconfiguration (graphql_orm.repositorytag).- The mapping attributes of
GraphqlOrm\Mapping, as DoctrineDoctrine\ORM\Mapping:#[Entity]and#[Field];#[Identifier]marks the identifier field, as Doctrine#[Id];#[Relation]maps a relation or a collection, withmappedFrom,targetEntity,fetchandignoreValidation.graphqlorm:make:entitygenerates them. A property holding an entity mapped with#[Field], or mapped with two field attributes, throws anInvalidArgumentException. GraphqlManagerInterface, as DoctrineEntityManagerInterface, withgetRepository()andclear(): the type to inject, and to simulate or decorate in the tests of an application. Autowired toGraphqlManager.
Changed
- The
query_builderoption ofGraphqlEntityType, the controller argument resolver andgraphqlorm:finduse the repository of the application: its own methods and overrides apply there too.
Deprecated
#[GraphqlEntity]and#[GraphqlField]ofGraphqlOrm\Attribute: use#[Entity]and#[Field]ofGraphqlOrm\Mapping. Theidentifierargument of#[GraphqlField]: add#[Identifier]. Mapping a relation with#[GraphqlField], and itstargetEntityandfetcharguments: use#[Relation].GraphqlOrm\Attribute\Fetch: useGraphqlOrm\Mapping\Fetch, of which it is an alias. All still work in 4.x and are removed in 5.0, see "Upgrading from the mapping of 4.8" in docs/entity-mapping.md. The PHPStan extension declares theFetchalias.Direction::ASCandDirection::DESC: useDirection::AscandDirection::Desc, the cases being named in PascalCase as the other enums of the bundle. The old names are constants of the enum, pointing to the new cases, so thatDirection::ASC === Direction::Asc; they are removed in 5.0. The values sent (ASC,DESC) are unchanged.- Not setting
identity_map,max_depth,variables.enabledandschema_validation.mode: their default value changes in 5.0 (true,1,true,warningin debug mode). Set them, see "Defaults changing in 5.0" in docs/configuration.md. - Extending
GraphqlManager: it is@finalsince 4.9 and final in 5.0, reported by the Symfony debug class loader. Depend onGraphqlManagerInterfaceto simulate or decorate it.
[4.8.0] — 2026-10-13
Added
identity_map: trueshares the entities between the queries of a request: an entity is one instance, updated by each query returning it, andfind()returns one known in full without a query. Reset between requests and Messenger messages (kernel.reset);GraphqlManager::clear()for a long-running script.toIterable()keeps its pages out of it. Disabled by default.#[GraphqlField(fetch: Fetch::Lazy)]: a relation selected by its identifier only, whatevermax_depth, and loaded on first access, the ghosts of a response together, as Doctrinefetch: LAZY. It needs nolazy_loading.GraphqlQuery::batch(): several queries on different root fields in one GraphQL document, one HTTP request. A root field in error fails its query only; paginated queries and queries on the same root field are rejected.
Changed
- The AST of each query is exported for the trace without a JSON round trip: about a quarter less of the time the bundle spends on a small query, measured by the new benchmarks (
tests/Benchmark, PHPBench).
[4.7.0] — 2026-10-13
Added
- A list criterion of the find methods,
count()and the aggregates matches any of its values, as with Doctrine:findBy(['id' => [1, 3]])is sent asin(Data API Builder) or_in(Hasura); an empty list matches nothing.PostGraphileDialectrejects it with aLogicException, its conditions matching a single value. slow_query_threshold(milliseconds): a query lasting at least that long is logged as a warning ("Slow GraphQL query") and highlighted in the profiler, with a count in the toolbar. No threshold by default.graphqlorm:findconsole command: queries the API withfindBy()(--criteria field=value,--order-by,--limit) and shows the entities as hydrated, as a table or JSON.
Changed
- The duration of a query is measured by the client itself, so that it is known without the Symfony stopwatch, which is still fed for the profiler timeline.
Fixed
findBy($criteria, []): an empty ordering, accepted by Doctrine, was sent as an emptyorderByargument, which Data API Builder rejects.GraphqlManager::send(), added in 4.6.0, was flagged@internal, which made the Symfony debug class loader report a deprecation for every class extending or mocking the manager.
[4.6.0] — 2026-10-13
Added
mappedFromof#[GraphqlField]is optional: the field is then named from its property by the naming strategy,naming_strategyin the configuration, as Doctrine does for columns.DefaultNamingStrategy(the default) keeps the property name,SnakeCaseNamingStrategyturnsdueDateintodue_date; implementNamingStrategyfor another convention. The PHPStan rules take the same class in thegraphqlOrm.namingStrategyparameter.- Query builder filters through relations:
$expr->eq('user.name', 'User B')is sent nested, as{user: {name: {eq: ...}}}; on a collection (tasks.done), it matches the entities having at least one related entity that matches. Data API Builder and Hasura. GraphqlEntityRepository::sum(),avg(),min()andmax(), aggregating a field over the entities matching the criteria, with the dialects implementing the newAggregatingDialectinterface:DataApiBuilderDialect(numeric fields) andHasuraDialect. The PHPStan rules check their field and criteria. A repository already declaring a method of the same name must give it the same signature, or rename it.GraphqlQuery::concurrently(), running several queries at once: every request is sent before any response is read.GraphqlClientimplements the newConcurrentGraphqlClientInterface; other clients run the queries one after the other.GraphqlQueryandPaginatedResultare covariant in their entity type: aGraphqlQuery<Task>is aGraphqlQuery<object>.
[4.5.0] — 2026-10-13
Added
- The PHP property names of an entity are accepted wherever a field is named, besides the GraphQL names: criteria and ordering of the find methods and
count(), query builder filters and ordering,#[MapGraphqlEntity]mapping.findBy(['dueDate' => null])sendsdue_date. A property having the GraphQL name of another field designates that field, as before. - A failing query naming fields the entity does not map points them out, with the closest mapped field:
Field "titel" is not mapped on App\Task: did you mean "title"?. They are still sent, the API may accept them. - Profiler: "Copy as cURL" copies a query, with its variables, as a command to replay it from a terminal. Headers are not recorded, so no token ends up in it.
GraphqlQuery::getSingleResult(), the only entity a query returns, throwing aNoResultExceptionwhen there is none and aNonUniqueResultExceptionwhen there are several, as Doctrine.
Changed
- PHPStan rules: a PHP property name is no longer reported where a field is named; the
graphqlOrm.propertyNameidentifier is gone (remove the@phpstan-ignore graphqlOrm.propertyNamecomments, if any).
Fixed
- An invalid query answered with an HTTP
400(Data API Builder, PostGraphile) threw "answered with HTTP status 400" without the errors of the server: they are now in the exception message and the profiler.
[4.4.0] — 2026-10-13
Added
- Query builder comparisons:
gt(),gte(),lt(),lte()(_gt... with Hasura),isNotNull()andnotIn(), sent as one inequality per value, Data API Builder having no "not in" operator. GraphqlQuery::toIterable(), a generator going through every page of a paginated query, fetching each page once the previous one is read.- Controller arguments typed with an entity receive the entity whose identifier is in the route (
{task}or{id}), a 404 response when it does not exist, ornullfor a nullable argument.#[MapGraphqlEntity]reads the identifier from another route parameter (id), finds the entity by other fields (mapping) or disables the resolution (disabled), as Doctrine#[MapEntity]does. GraphqlEntityType, a form field whose choices are entities, loaded withfindAll()or thequery_builderoption, as DoctrineEntityType. Registered when symfony/form is installed.MockGraphqlServer, set as thehttp_clientof the bundle in the tests of an application: it answers the queries with the responses queued (respond(),respondWithErrors(),respondWithHttpStatus()) and records them.
Changed
- Documentation: retrying temporary errors. Symfony does not retry
POSTrequests on network errors, timeouts,500or504by default; the configuration of the "HTTP client" section now listsPOSTfor these codes.
[4.3.0] — 2026-10-13
Upgrading from 4.2
- A repository already declaring its own
count()must give it the signaturecount(array $criteria = []): int, or rename it: otherwise PHP fails with a fatal error on loading it. - Schema validation is stricter:
graphqlorm:schema:validatemay now fail on mappings it accepted, e.g. a nullable field mapped on a non-nullable property. Each message says how to fix the mapping.
Added
- The profiler reports repeated queries, those differing only by their values and sent 3 times or more (the sign of an N+1 problem), in yellow in the toolbar and listed at the top of the panel.
GraphqlEntityRepository::count($criteria), with the dialects implementing the newCountingDialectinterface:DataApiBuilderDialect(aggregation),HasuraDialect(_aggregate),PostGraphileDialectandRelayDialect(totalCount). The PHPStan rules check its criteria as those offindBy(). A repository already declaring its owncount()must give it the same signature,count(array $criteria = []): int(that of Doctrine), or rename it.- Schema validation (
graphqlorm:schema:validate, first request listener) also reports a field the schema declares nullable mapped on a property that does not acceptnull, a relation to another type than the one of its target entity or mapped on a scalar, and a collection mapped on a single object (or the reverse).
Fixed
- The profiler template defined its AST macro inside a block, deprecated since Twig 3.28 and an error with Twig 4.
[4.2.0] — 2026-10-13
Added
PostGraphileDialect: Relay connections, criteria of the find methods sent as acondition, ordering with theorderByenum values of the fields (DUE_DATE_DESC); the query builder filters, which need a plugin, are rejected.EnumLiteral, an enum value built at runtime, printed without quotes by the walkers and sent as a string in the variables.HasuraDialectsupportspaginate()with offsets: the cursor of a page is the offset of the next one, and one extra row is requested so thathasNextPageis exact.OffsetPaginationDialectmarker interface for such dialects.
Fixed
DataApiBuilderDialect: going back to the first page of a pagination started at a cursor (paginate($cursor)) sent that cursor again, returning the same page instead of the first one.
[4.1.1] — 2026-10-13
Fixed
DefaultDialectwrapped nested collections in anitemsenvelope, a Data API Builder syntax that standard GraphQL APIs reject: they are now selected as plain lists.- Schema validation reported a mismatch, and
make:entity --from-schemagenerated astring, for the scalars named after the database types by Hasura (timestamptz,numeric,bigint...) and for theDatetimeof PostGraphile. make:entity --from-schemaused the table name of Hasura types as relation target class (users): it is now a singular class name (User).
Changed
symfony/stringis required: entity generation uses its inflector (it was already installed bysymfony/console).
[4.1.0] — 2026-10-13
Added
- Lazy loading loads the ghosts of a class met in a response with one query (
inon their identifiers, 100 per query) instead of one query each, with the dialects implementing the newBatchLoadingDialectmarker interface:DataApiBuilderDialectandHasuraDialect.
Changed
GraphqlExecutionContext::$lazyLoader, set by the manager, loads a list of identifiers and is marked@internal.
[4.0.1] — 2026-10-13
Fixed
- Lazy loading: a ghost referenced twice in the same response (two entities pointing to the same relation) was marked as loaded with its default values, without query. A ghost is now only completed by a row holding every mapped field; otherwise it is loaded on first access.
[4.0.0] — 2026-10-13
Upgrading from 3.x
- PHP 8.4+ is required: lazy loading relies on PHP 8.4 lazy objects. The Symfony requirement is unchanged (7.4 or 8.x).
- Nothing else changes unless
lazy_loadingis enabled: it is disabled by default, so relations beyondmax_depthstill only get their identifier. When enabling it, mind the extra query sent for each lazy relation accessed (see the "Lazy loading" section of docs/configuration.md). - Contributors: the Castor QA tasks need Castor 1.8 or later, whose embedded PHP satisfies the requirement.
Added
lazy_loadingoption (disabled by default): the relations reduced to their identifier beyondmax_depthare hydrated as PHP 8.4 lazy ghosts, loaded with a query of their own on first access;EntityNotFoundExceptionwhen the entity no longer exists.
Changed
- Requires PHP 8.4+ (was PHP 8.3+).
[3.2.0] — 2026-10-13
Added
RelayDialect, for APIs following the Relay cursor connections specification (edges { node },first/after,pageInfo), with itsRelayGraphqlWalker.HasuraDialect, for Hasura and APIs following its conventions:wherewith_-prefixed operators (%and_of searched values escaped),order_by,limit, plain lists;paginate()is rejected, Hasura paginating with an offset.- PHPStan rules (
extension.neon, enabled by phpstan/extension-installer) checking the field names given tofindBy(),findOneBy()and the query builder against the entity mapping: PHP property name instead of GraphQL name, unmapped field with a suggestion (graphqlOrm.reportUnmappedFieldsto allow them), path through a field that is not a relation. CursorPaginationDialectinterface, telling wherehasNextPageandendCursorlie; implemented byDataApiBuilderDialectandRelayDialect.AbstractGraphqlWalker::walkCollection(), to print the envelope of nested collections.- Schema validation and
make:entity --from-schemaunwrap Relay connections.
Changed
phpstan/phpdoc-parseris required in^2.2(was^2.0), the first version compatible with PHPStan 2.2 which shares it.DefaultGraphqlWalkeris no longerfinal, so that dialects close to standard GraphQL can extend it.
[3.1.1] — 2026-10-13
Fixed
graphqlorm:make:entityno longer adds@methodannotations to the generated repository: they described the 2.xfindBy()signature and missedfind()andfindOneBy(), while@extends GraphqlEntityRepository<Entity>already types every method. Existing repositories can drop them as well.- The description of
graphqlorm:make:entitywas in French, unlike the other commands. - Schema validation on the first request was remembered in a static property, shared by every container of the process: a second kernel (functional tests, a rebuilt container in worker mode) skipped it. It now runs once per container.
[3.1.0] — 2026-10-13
Added
graphqlorm:make:entity --from-schema: generates the entity properties (types, nullability, identifier, relations and collections) from the GraphQL schema instead of asking them.- Query events (
QueryStartedEvent,QueryExecutedEvent,QueryFailedEvent, all extendingQueryEvent) dispatched through the Symfony event dispatcher, with the query values masked. New dependency:psr/event-dispatcher. GraphqlQuery::getArrayResult(): rows as the API sent them, without hydration, the collection envelopes of the dialect removed.
Fixed
- Queries without variables sent
"variables": [], which Data API Builder 2 (Hot Chocolate) rejects with an HTTP 400: variables are now always sent as a JSON object. - An entity first met as a nested relation reduced to its identifier (beyond
max_depth) was returned as is when its complete row came later in the same response, leaving its other properties uninitialized: it is now completed, and its#[AfterHydrate]hooks run once it is.
[3.0.0] — 2026-10-12
Upgrading from 2.x
- PHP 8.3+ and Symfony 7.4 or 8.x are required.
- Logs and profiler show
***instead of query values: setredact_query_values: false(e.g. underwhen@dev) to see them. DefaultDialectthrows aLogicExceptiononorderBy()andpaginate()instead of ignoring them: configure the dialect matching your API.- With
DataApiBuilderDialect,findBy()criteria are sent as an equalityfilterinstead of plain root arguments. - A repository overriding
findBy()must use the new signaturefindBy(array $criteria, ?array $orderBy = null, ?int $limit = null): array. A repository declaring its ownfind()orfindOneBy()with another signature must rename it or align it on the new methods. - A custom dialect must implement
GraphqlQueryDialect::applyCriteria(); a custom walker extendingAbstractGraphqlWalkermust implementwalkRootField(). GraphqlManagerproperties are private: usegetMetadataFactory(),getHydrator(),getMaxDepth()andgetDialect(), and pass the dialect to the constructor.GraphqlClient,GraphqlQueryCompiler,GraphqlExecutionContextandGraphqlOrmDataCollectorarefinal: decorateGraphqlClientInterfaceinstead of extending the client.- Classes marked
@internalmay change in any release: do not use or extend them. GraphqlManagerand#[GraphqlEntity]are no longer generic: removeGraphqlManager<...>type parameters from PHPDoc.- Outside debug mode, entity metadata is cached in
cache.system: clear or warm up the cache when deploying, as for the rest of the Symfony cache.
Added
- Entity metadata is cached between requests outside debug mode (
metadata_cache_pooloption,cache.systemby default) and warmed up bycache:warmup. headers_provideroption: id of a service implementingHeadersProviderInterface, whose headers are computed before every request (e.g. an OAuth access token) and override the staticheaders.GraphqlEntityRepository::find($id)andfindOneBy($criteria, $orderBy), returning the entity ornull.http_clientoption: id of the HTTP client service to use, e.g. a scoped client with a timeout and retriesvariablesoption (disabled by default): root query arguments are sent as typed GraphQL variables, their types being introspected from the schema and cached (variables.cache_pool,variables.ttl). Values are sent inline when the schema cannot be introspected or an argument is unknown. New dependency:symfony/cache-contracts.redact_query_valuesoption (defaulttrue): query values are masked in logs and in the profiler, as they may hold personal data. The query sent to the API is not affected.
Changed
GraphqlManager, and the#[GraphqlEntity]attribute, are no longer generic: a repository constructor can typeGraphqlManager $managerwithout PHPStan asking for a type parameter.GraphqlManager::execute()takes the entity type from its hydration callable.- Requires PHP 8.3+ and Symfony 7.4 or 8.x (was PHP 8.2+ and Symfony 7.1+): Symfony 8 is now supported, and the Symfony versions no longer maintained are dropped.
GraphqlEntityRepository::findBy()takes an optional ordering and limit (findBy($criteria, $orderBy, $limit)): a repository overridingfindBy()must update its signature.GraphqlQueryDialectdeclaresapplyCriteria(), which turns the criteria of the repository find methods into root arguments: custom dialects must implement it.DataApiBuilderDialectnow sends them as an equalityfilter(isNullfornull) instead of plain root arguments, which Data API Builder rejected;DefaultDialectkeeps plain root arguments.GraphqlManagerdependencies are now private: usegetMetadataFactory(),getHydrator(),getMaxDepth()andgetDialect()instead of the former public properties, and pass the dialect to the constructor instead of assigning$manager->dialect.GraphqlClient,GraphqlQueryCompiler,GraphqlExecutionContextandGraphqlOrmDataCollectorare nowfinal: decorateGraphqlClientInterfaceinstead of extending the client.- Implementation classes are marked
@internaland are not covered by the backward compatibility promise: the query compiler and string builder, the hydrator, the metadata classes, the schema tools, the query redactor, the commands, the event listener, the data collector and the DependencyInjection classes. The public API is made of the attributes,GraphqlManager, the repository, the query builder and query, the dialects, walkers and AST, the client interface, the expressions and the exceptions. DefaultDialectthrows aLogicExceptiononorderBy()andpaginate(), which it used to silently ignore: standard GraphQL defines no ordering nor pagination arguments. Use a dialect matching your API.AbstractGraphqlWalkernow implementswalk()and declares an abstractwalkRootField(): a custom walker extending it must implementwalkRootField(). Walkers implementingGraphqlWalkerInterfacedirectly are not affected (they just do not support variables).- Logs and profiler show
***instead of query values by default; setredact_query_values: false(e.g. underwhen@dev) to see them.
Fixed
PaginatedResult::next()returnsnullwhen the API reports a next page without an end cursor, instead of loading the first page again (which made a loop overnext()endless).- A collection row that is not an object throws an
InvalidGraphqlResponseExceptioninstead of a PHPTypeError.
[2.0.0] — 2026-10-12
Upgrading from 1.x
GraphqlClientnow throwsGraphqlRequestException(aGraphqlOrmException) on transport failure, HTTP status= 400, non-JSON body, or GraphQL errors without data. Code relying on an empty result, or catching Symfony HTTP client exceptions, must catch
GraphqlRequestExceptioninstead.DefaultDialectfilters are now printed as input objects:eq('status', 'active')generatesstatus: { eq: "active" }(it generated the invalidstatus: ["active"]).- An argument or input object name that is not a valid GraphQL name throws
InvalidArgumentException. max_depth(default2) is now applied: the automatic selection loads fewer nested relations than before. Raise it, or use an explicitselect(), to load deeper relations.getOneOrNullResult()sendsfirst: 1when no limit is set: the API must accept afirstargument.- New dependencies:
symfony/type-infoandphpstan/phpdoc-parser.symfony/console^7.3is required. QueryOptions::$filterwas removed (it was never read).GraphqlManager::$collectoris nullable, the profiler collector only exists in debug mode, andEntityHydratoris no longer areadonlyclass.
Added
graphqlorm:schema:validatecommand, to validate the mapping against the schema in CISchemaIntrospector::introspectSchema()also returns the name of the root query type- Collection relations are detected from the property docblock (
@var Task[],list<Task>), throughsymfony/type-infoandphpstan/phpdoc-parser - Hydration of
BackedEnum,DateTimeandDateTimeInterfaceproperties
Changed
GraphqlClientthrowsGraphqlRequestExceptionon transport failure, HTTP status >= 400, non-JSON body, or GraphQL errors without data. Partial data alongside errors is still returned, errors being recorded in the query trace.getOneOrNullResult()requests a single result (first: 1) when no limit is set- The profiler collector is only registered in debug mode;
GraphqlManager::$collectoris nullable EntityHydratorcaches reflection per class and is no longer areadonlyclass- Removed the unused
QueryOptions::$filterproperty - CI: tests also run against the lowest dependencies, and sources are linted on PHP 8.2
Fixed
- GraphQL injection: string values are now escaped and argument / input object names are validated (
InvalidArgumentExceptionon an invalid name) DefaultDialect: filters were printed as lists (status: ["active"]) instead of input objects (status: { eq: "active" })DataApiBuilderDialect: nested collections were hydrated with theiritemsenvelope instead of the related entities- Hydration: invalid
float,boolandstringvalues now throwCastExceptioninstead of aTypeError(or, forstring, silently storing the exception text) - Identity map was ignored when the identifier was not the first mapped property
- PHP 8.2 compatibility: removed a typed class constant (PHP 8.3 syntax)
symfony/consolerequirement raised to^7.3, needed by the invokable commands and#[Argument]/#[Option]- Documentation:
Direction::DESCcase andgetGraphQL()example output max_depthwas ignored: relations beyond it are now reduced to their identifier in the automatic selection- Metadata: a
targetEntityon a union-typed property crashed, and untyped mapped properties were silently ignored - Pagination: fetching another page altered the options of the original query; a
nullroot returned an array instead of an emptyPaginatedResult - Profiler: variables, root result count and hydration depth were never recorded
- Schema validation: the GraphQL type is resolved from the root query field (including Data API Builder connections) instead of being guessed by removing a trailing "s"
- Schema validation: Data API Builder scalars (
Byte,Short,Long,Single,Decimal) were reported as mismatches - Schema introspection:
[Type!]!fields were not fully unwrapped InvalidGraphqlResponseExceptionnow implementsGraphqlOrmException- Invalid dates now throw
CastException - Profiler caller: the path was shortened against a wrong project directory, frames were filtered on a
graphql-ormsubstring (hiding projects whose path contains it), and the recorded class/function was the called one - The container no longer needs the
filesystemservice from FrameworkBundle - Non-finite floats (
INF,NAN) are rejected instead of producing an invalid query graphqlorm:make:entityrejects property and GraphQL field names that would generate invalid code- An entity reused from the identity map is no longer counted twice in the hydration stats
[1.0.0] — 2026-02-28
Added
Hydration hooks
#[BeforeHydrate]— method hook called before field assignment, receives raw GraphQL data array (802e32b)#[AfterHydrate]— method hook called after field assignment, for computing virtual fields and post-processing (802e32b)#[AfterHydrate]is skipped on partial hydration to prevent accessing uninitialized properties
Schema validation (1b3a404)
- Introspection-based schema validation via
SchemaIntrospectorandSchemaValidator - Checks: GraphQL type existence, field existence, PHP/GraphQL scalar compatibility
- Three modes:
exception,warning,disabled - Entity classes discovered automatically from
mapping.entity.dir - Triggered on first HTTP request via
SchemaValidationListener
Field suggestions (1474c8c)
- "Did you mean?" suggestions using Levenshtein distance when a mapped field is not found in the schema
Logging (e6d189b)
- Dedicated
graphql_ormMonolog channel debuglevel for successful queries (endpoint, duration, hydration stats, query, caller)errorlevel for queries returning GraphQL errors
Query duration (843d0a2)
- Query duration measured via Symfony Stopwatch
- Duration exposed in the Symfony Profiler timeline per query
Debug command (ca7b587)
graphqlorm:debug:entity— displays resolved metadata for an entity (table or JSON output)
Pagination (54a7fde)
- Cursor-based pagination via
->paginate() PaginatedResult<T>withitems,hasNextPage,hasPreviousPage,endCursor->next()and->previous()navigation helpers
Query Builder (c149e49)
ExpressionBuilderwitheq,neq,contains,notContains,startsWith,endsWith,in,isNull,andX,orX- Dot notation for nested field selection (
user.name) orderBy(),limit(),addSelect()- Raw GraphQL query via
setGraphQL() - Query inspection via
->getQuery()->getGraphQL()
AST (fde7559)
- AST-based query generation with
QueryNode,FieldNode,SelectionSetNode - Walker pattern with
DefaultGraphqlWalkerandDABGraphqlWalker
Dialects (20c9dcb)
DefaultDialectfor standard GraphQL APIsDataApiBuilderDialectfor Microsoft Data API Builder withitemsenvelope and cursor paginationGraphqlQueryDialectinterface for custom implementations
Entity generator (429a1cc)
graphqlorm:make:entity— interactive wizard generating entity + repository- Autocomplete on existing entities for relation targets
Profiler (f5e2a49)
GraphqlOrmDataCollectorwith per-query details: endpoint, duration, response size, hydration stats, AST viewer, variables, caller- GraphQL errors highlighted in the toolbar
CI (7faff8e)
- GitHub Actions CI pipeline
Tests (eb6115f)
- PHPUnit test suite covering hydration, metadata, schema validation, introspection, hydration hooks
Fixed
- Nested collection relations missing
itemswrapper in generated query forDataApiBuilderDialect(582a0d8) - Empty error message when query had no arguments (
352c3c1) - Deprecated use of
Symfony\Component\HttpKernel\DependencyInjection\Extension(a306a4d)
Changed
- HTTP client options
verify_hostandverify_peermade configurable (4f483ec) - Documentation moved from
README.mdtodocs/directory (6548bda) - Symfony requirement updated to
^7.1for#[Argument]and#[Option]console attribute support - PHP requirement set to
^8.2forreadonly classsupport
[0.1.0] — 2026-02-21
Initial commit — core entity mapping, hydration, identity map, repositories, and cycle-safe relation loading.