A filter value of a smallint, integer or bigint field, or of an
association to an identifier of one, beyond its column’s range, such as
40000 for a smallint, was a database error on PostgreSQL and matched
nothing on others. It is now an error,
Filter 'eq' of field 'small' must be an integer from -32768 to 32767. On
MySQL and MariaDB a field with the unsigned option has the unsigned range.
Resolve\ResolveCollectionFactory no longer takes a FieldResolver or a
TypeContainer, and Filter\FilterFactory no longer takes an
EventDispatcher; neither used them. Code which constructs them, rather
than getting them from the Driver, passes the other arguments.
A to-one association is filtered by an ID, which may be any string. A
value which is not an integer, for an integer identifier, matched nothing on
SQLite and MySQL and was a database error on PostgreSQL. It is now an error,
Filter 'eq' of field 'artist' must be an integer.
The excludeFilters and includeFilters of an #[Association]
attribute did not limit the filters of a to-one association by the identifier
of the entity it refers to. They now do, so such an association may have
fewer filters. An eventName or a limit on a to-one association, which
were silently ignored, now throw
ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Metadata; they apply to a
collection only.
Every exception of this library extended webonyx’s GraphQL\Error\Error
and was client safe, so a client saw the message of an error the developer
must fix, which may name classes, methods and groups. Types, hydrators and
inputs are built when they are first used, often while a query is validated
or executed, so such an error reached the client even when the schema had
been built first. Exception\GraphQL::isClientSafe() now returns false,
and a client sees Internal server error.
Exception\Filter, Exception\Pagination and
Exception\TypeSerialization, the errors of a client’s request, extend the
new Exception\ClientError and are still shown. A scalar’s error names its
GraphQL type, such as DateTimeImmutable, rather than its Doctrine type,
such as datetime_immutable. Invalid JSON and a DateInterval too large
for PHP were shown as Internal server error; the client is now told
Json is not a valid JSON document. or DateInterval ... is out of range. The
Filter type name ... is already used for different filters error is the
developer’s, and now throws Exception\Configuration rather than
Exception\Filter. See errors to log hidden errors.
A negative limit of an #[Entity] or #[Association] attribute
returned no rows when no first or last was given, and let first
return more rows than the config limit. It now throws
ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Metadata. 0 remains
the default.
A #[Field] attribute on a property which is not a mapped field, such as an
association, an embeddable or a property with no column, an
#[Association] attribute on a property which is not an association, and a
#[ComputedField] attribute on a method which is not public, or is static,
were silently ignored. Each now throws
ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Metadata when the metadata
is built. Only the attributes of the driver’s group are checked. Move the
attribute to where it applies, or remove it.
An #[Association] to an entity with no #[Entity] attribute of the
driver’s group failed every query of its entity’s type, even one which did
not ask for the association, with an error naming only the entity it refers
to. It now throws ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Metadata
naming the association when the metadata is built, for every exposed entity,
including one which no schema uses. Expose the entity it refers to in the
group, or remove the attribute.
A field in the required or optional fields of $driver->input() which is
not a field of the entity, is not exposed in the group or is an identifier
was reported only when a query used the input type, as an error returned to
the client. input() now throws
ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Input for it, so the
mistake is found when the schema is built.
A filter on the inverse side of a one-to-one association was offered, but Doctrine cannot compare that side, which has no column, so the query always failed. The filter is removed. Filter by the owning side’s entity instead.
A collection whose association has an #[ORM\OrderBy] attribute was
ordered by the target’s identifier, unlike the collection Doctrine loads. It
is now ordered by the OrderBy, after any sort filter and any ordering
of a QueryBuilder event listener, and then by the identifier.
Extracting by value, the default, the hydrator reads each field with its
getter and silently left out a field without one, which was then always
null. Building the type of an entity with an exposed field or association
which has no getField() or isField() method, and no __call, now
throws ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Hydrator. Add the
getter, or extract the entity by reference with extractByValue: false.
The hydrator calls the getter of every mapped field and association, exposed
or not, without arguments. A getter which requires a parameter failed every
query of its entity’s type, even one which did not ask for that field, as an
Internal server error. It now throws the same exception when the type is
built. Give its parameters default values, or extract by reference.
A DateTime or DateTimeImmutable value sent with an offset other than
PHP’s default timezone kept that offset. A datetime column stores the
date and time without an offset, so a filter compared, and an input stored,
the wrong instant: with the default timezone UTC, 15:19:21+05:00 matched,
and was stored as, 15:19:21 UTC rather than 10:19:21 UTC. Such a value is now
converted to the default timezone, the same instant. DateTimeTZ values
keep their offset.
The DateTime, DateTimeImmutable, DateTimeTZ and
DateTimeTZImmutable scalars rejected a value with a fraction of a second,
such as 2004-02-12T15:19:21.123Z from JavaScript’s Date.toISOString().
It is now accepted; digits after the sixth are dropped. A value with a
fraction of a second was serialized without it. It is now serialized with
six digits, such as 2004-02-12T15:19:21.123000+00:00. A value of whole
seconds is serialized as before.
ConfigBuilder::sortFields() is renamed enableSortFields(), and its
parameter is named $enable, as the other boolean methods’ are.
// 13.x
ConfigBuilder::create()->sortFields();
// 14.0
ConfigBuilder::create()->enableSortFields();
The sortFields config value is a bool, false by default. It was
bool|null, where null, the default, meant false. Passing
'sortFields' => null to Config now throws
ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Configuration; remove it or
pass false.
A computed field of a method such as getaway(), whose name only begins
with get, is named for the whole method, getaway, rather than away.
Attribute excludeFilters and includeFilters accept the values of
filters, such as 'eq', as Config does; an unknown filter throws
ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Configuration rather than a
TypeError.
A typeName, alias or computed field name which is not a valid GraphQL
name, or a group or groupSuffix which makes type names invalid, built a schema
which webonyx rejects only when the schema is validated, which it is not by
default. Such a name now throws
ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Metadata naming the entity
and the name.
The Driver constructor’s arguments were public read-only properties:
$driver->entityManager, $driver->config and $driver->metadataArray.
$driver->config was null when no Config was given, although the
driver used a default one. They are no longer properties. Get the services
from the driver instead:
$driver->service(EntityManager::class);
$driver->service(Config::class);
$driver->service(Metadata::class);
Driver::type() returns a GraphQL\Type\Definition\Type rather than
mixed, Driver::filter() an InputObjectType rather than object,
and Driver::connection() and Driver::dbalConnection() a
Type\Connection rather than an ObjectType. The values are unchanged;
code which checked their types may drop the checks.
Filters are built only for fields of a scalar type, so Filters::type(),
Field::nameFor() and the constructors of the Field, Association and
Between filter input types take a ScalarType rather than a
ScalarType or ListOfType. The unused list type branches, which named
a type with uniqid(), are removed. Filters::type() returns the
Between type of a scalar type shared through the TypeContainer, as the
filter factory did, rather than a new one.
Every connection ran a count query. A forward page (no last or
before) which does not request totalCount is now resolved without one;
one row more than the page is fetched to tell whether there is a next page.
The GraphQL results are unchanged. A batched collection still counts its rows
with one query for the batch.
Code which calls a resolver directly and reads totalCount from the array it
returns gets null when the query did not request it.
PaginationService::buildPaginationResponse() takes a nullable
$totalCount and an optional $hasNextPage, and
ResolveDbalFactory::buildPagination() an optional ResolveInfo.
A field’s filter type is named by its type and a hash of its filters. The
hash is now 8 characters rather than 32, as input type names use, so
Filters_String_9e7d5d723262a26bad04896d55cce1e1 becomes a name such as
Filters_String_8e869526. This is a schema change; regenerate client types
built from the schema.
Config checked the names of its options but not their values. A value of
the wrong type threw PHP’s TypeError, a limit of 0 or less removed the
limit, and a negative batchLimit was accepted. Each value is now checked
and an invalid one throws ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Configuration
naming the option: limit must be at least 1, batchLimit at least 0,
group may not be empty, and each of excludeFilters must be a
Filters case or its value. Config::getExcludeFilters() returns
Filters cases, as documented, when they were given as values.
A connection type was registered in the TypeContainer by the name of the
type it connects rather than its own, so $driver->type() of an entity’s
type name returned its connection, and dbalConnection() of a row type named
as a registered type, such as datetime, returned that type. A connection
is now registered by its own name, Connection_ and the name of the type it
connects. Type names in the schema are unchanged.
Code which builds a connection itself passes the connection’s name:
use ApiSkeletons\Doctrine\ORM\GraphQL\Type\Connection;
// 13.x
$typeContainer->build(Connection::class, $objectType->name, $objectType);
// 14.0
$typeContainer->build(Connection::class, Connection::nameFor($objectType->name), $objectType);
An alias which was the name of another field, or a computed field named as
an alias, silently replaced the other field in the type. Every field of a
type, named by its alias if it has one, must now have a unique name; a
duplicate throws ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Metadata
naming both fields. The message for two equal aliases changes from
Duplicate alias to Duplicate field name.
$driver->input(Entity::class) without field lists made every field
required, including fields whose column is nullable. Those fields are now
optional. This is a schema change.
The required and optional field lists accept a field’s alias, the name the
input uses, as well as its field name, and name the same input type either
way. A field in both lists, which was silently optional, throws
ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Input.
A decimal field was a Float, which rounds a value of many digits.
Doctrine reads a decimal as a string, and it is now a String, which keeps
its precision, with the ToString strategy. It keeps the number filters,
and a comparison value which is not a number is an error. This is a schema
change: clients receive, and send as filter values, strings such as
"314.15". The DBAL 4 number type has the number filters too.
A bigint field is a String, so it was given the string filters,
including contains, startswith and endswith but not lt,
lte, gt, gte or between. It now has the number filters, and a
comparison value which is not an integer is an error. This is a schema change.
eq: null, neq: null, in or notin given null or a list
containing null, and between with a null or missing from or to
compared to null in SQL, which matches nothing, so they silently returned no
rows. They are now an error which names the filter and field. Use
isnull to match null values.
A date_immutable field was given contains, startswith and
endswith, which no other date or time field has. They are removed. This
is a schema change.
A JSON field was given every filter, but none except isnull worked: eq
failed with a SQL error and the others compared decoded JSON with the stored
JSON text. Its filter type now has only isnull. This is a schema change.
A field whose filters are all excluded is now left out of the filter type
rather than given an input type with no fields, which is invalid.
The Date, DateTime and DateTimeTZ scalars, and their immutable
versions, accepted a date or time which does not exist and rolled it over:
2004-02-31 became 2004-03-02 and T25:00:00 the next day. Such a value
is now an error.
contains, startswith and endswith passed their value into a LIKE
pattern unescaped, so % and _ in the value acted as wildcards:
contains: "%" matched every non-null value. They are now escaped and match
only themselves. A client which relied on sending wildcards must use another
filter.
With useQueryResultCache enabled, cached results were kept for the life of
the driver. They were served after the entity manager was cleared and after a
mutation flushed, so a long running process returned stale, detached entities
and the cache grew without limit. The cache is now cleared whenever the entity
manager is cleared or flushed. Clear the entity manager between requests, as
a long running process should already do.
Metadata is now cached with $driver->get(Metadata::class)->toArray(),
which adds a __version key to the array, rather than getArrayCopy().
The Driver requires that key when it is given cached metadata, and throws
ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Metadata without it.
Regenerate metadata caches written by 13.x or with getArrayCopy().
Change the caching code to use toArray(); see metadata.
Add '__version' => Metadata::FORMAT_VERSION and '__config', the
Metadata::configOf($config) values of the config the metadata is for, to
metadata you write by hand.
The export records the config values the metadata depends on: group,
groupSuffix, entityPrefix and extractByValue. A driver given
cached metadata built with other values throws, rather than silently serving
another group’s types, names and limits.
Each entity’s byValue key is renamed extractByValue, as the Entity
attribute’s argument is; the shape of each entity’s metadata is otherwise
unchanged.
The globalByValue config option is renamed extractByValue, matching
ConfigBuilder::extractByValue(), and Config::getGlobalByValue() is
renamed Config::getExtractByValue(). Its meaning is unchanged. The old
name throws ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Configuration.
The byValue argument of the #[Entity] attribute is renamed
extractByValue too, and Attribute\Entity::getByValue() is renamed
getExtractByValue(), so the option has one name wherever it is set. The
old argument throws ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Metadata.
The parameter of ConfigBuilder::extractByValue() is renamed $enable, as
the other boolean methods’ are.
// 13.x
new Config(['globalByValue' => false]);
#[GraphQL\Entity(byValue: false)]
// 14.0
new Config(['extractByValue' => false]);
#[GraphQL\Entity(extractByValue: false)]
This library never used symfony/var-exporter, so it is no longer a
requirement. Doctrine ORM 3 still requires it and installs it. If you use
Doctrine ORM 2 with lazy ghost objects enabled, which need it, require
symfony/var-exporter in your own project.
This library now has its own
ApiSkeletons\Doctrine\ORM\GraphQL\Hydrator\Strategy\Strategy interface, which
extends Laminas\Hydrator\Strategy\StrategyInterface. Its extract() method
takes a third argument, the Doctrine field name being extracted:
public function extract(mixed $value, object|null $object = null, string|null $fieldName = null): mixed;
All strategies supplied with this library implement the new interface.
The abstract Hydrator\Strategy\Collection strategy is removed. It was a
copy of Doctrine’s internal AbstractCollectionStrategy. A custom strategy
for a collection-valued association must implement
Doctrine\Laminas\Hydrator\Strategy\CollectionStrategyInterface and this
library’s Strategy interface, as AssociationDefault does.
Custom strategies which implement only the Laminas interface continue to work unchanged and are called without the field name. See Hydrator Strategies.
The pagination argument is removed. first, after, last and
before are now top-level arguments of every connection, as the
GraphQL Complete Connection Model
defines them. This is a schema change.
Clients must move the pagination fields out of the pagination object:
# 13.x
artists (filter: { name: { contains: "Dead" } }, pagination: { first: 10, after: "MA==" }) { ... }
# 14.0
artists (filter: { name: { contains: "Dead" } }, first: 10, after: "MA==") { ... }
A query which still uses pagination fails validation with an unknown
argument error. Regenerate any client types built from the schema.
Schemas built by hand: Driver::pagination() now returns an array of the
four argument definitions instead of the Pagination input type. Add it to
the top level of the args rather than under a pagination key:
// 13.x
'args' => [
'filter' => $driver->filter(Artist::class),
'pagination' => $driver->pagination(),
],
// 14.0
'args' => ['filter' => $driver->filter(Artist::class)] + $driver->pagination(),
completeConnection(), dbalCompleteConnection() and the arguments of
association fields are updated for you.
The Pagination input type (Type\Pagination) is removed, so
$driver->type('pagination') no longer exists.
QueryBuilder event listeners: $event->getArgs()['pagination'] is now
always null. Read getArgs()['first'], ['after'], ['last'] and
['before'] instead. This change does not raise an error, so search your
listeners for 'pagination'.
An entity type created with a custom event name, through $driver->type(),
connection() or completeConnection(), was named
<type name>.<event name>. A . is not valid in a GraphQL name, so the
schema failed assertValid(), schema printing and client code generation.
The event name is now joined with an underscore, and any character which is
not valid in a GraphQL name is replaced with an underscore:
artist_default.artist.custom becomes artist_default_artist_custom.
The Node_ and Connection_ types change with it. The event dispatched
is unchanged. Regenerate any client types built from the schema.
An alias on a to-one association (ManyToOne or OneToOne) was
ignored when naming the GraphQL field, which kept the association name and
always resolved to null. The field and its eq filter are now named by the
alias, as they already were for collections and fields.
If you set an alias on a to-one association, query and filter it by the
alias. This is a schema change for those fields; regenerate any client types
built from the schema.
Several configuration checks used assert(), which is disabled in
production when zend.assertions is -1. With assertions disabled a
duplicate attribute was silently accepted, the last one winning. These checks
now throw in every environment:
Two #[Entity], #[Field], #[Association] or #[ComputedField]
attributes for the same group on one entity, field, association or method
throw ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Metadata, which was
an AssertionError.
A hydratorStrategy which does not implement
Laminas\Hydrator\Strategy\StrategyInterface throws
ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Hydrator.
Building a type with TypeContainer::build() from a class which does not
implement Buildable throws
ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Configuration.
A #[ComputedField] whose name collides with a field throws
ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Metadata, which was a
RuntimeException.
If your development environment ran with assertions enabled you have already seen these errors. Otherwise, fix any duplicate attributes they report.
The Date, DateImmutable, DateTime, DateTimeImmutable,
DateTimeTZ, DateTimeTZImmutable, Time and TimeImmutable
scalars now serialize any DateTimeInterface, mutable or immutable, and
throw ApiSkeletons\Doctrine\ORM\GraphQL\Exception\TypeSerialization
for anything else. Six of them previously returned a string unchanged and
returned null for any other value, including the other DateTimeInterface
class. If a resolver or computed field returns a formatted string for one of
these types, return the date object instead.
A Time or TimeImmutable value written as a literal in a query is now
validated, as a variable already was. An invalid time literal is an error.
Associations are now loaded in batches, which is on by default. Unloaded to-one associations are loaded with one query per entity class, and a collection field is resolved with a fixed number of queries however many rows it is resolved for: one for the identifiers of its rows, which give each row’s count and page, and one for the entities on the pages. Artists with their performances take 3 queries instead of 10, and two levels of collections 5 instead of 22. The results are the same.
A collection whose association has an eventName is not batched, so its
QueryBuilder event is still dispatched for each row with that row as
getObjectValue().
Resolvers now return GraphQL\Deferred for batched fields, which
GraphQL::executeQuery() resolves. If you execute queries with your own
promise adapter, it must support Deferred.
To turn batching off, set 'batchAssociations' => false. See
batchAssociations and batchLimit.
A many-to-many collection on the owning side returned every row of the target entity for every source, and one on the inverse side failed with a Doctrine semantic error. Both now return the members of the source’s own collection.
Entity connections and collections had no ORDER BY unless the sort
filter was used. SQL does not guarantee the order of rows without one, so
pages fetched with first and after could overlap or skip rows.
The identifier is now always the last ordering: rows without a sort are
returned in identifier order, and rows which sort equally are ordered by
identifier. Ordering added by a QueryBuilder event listener comes before the
identifier, so it still decides the order.
The includeFilters and excludeFilters of one #[Field] also removed
those filters from every field of the entity processed after it. A field now
has the entity’s filters limited only by its own attribute, so some fields
gain filters they were missing. This is a schema change: the filter input
types of those fields gain fields and their generated names change.
Regenerate any client types built from the schema.
Filter values for date and time fields are bound as the field’s Doctrine
type. A filter on a date, date_immutable, time or
time_immutable field previously matched no rows, because the value was
bound as a datetime. No change is needed; these filters now work.
The sort filter was a String. Any value was passed to Doctrine, so an
invalid direction failed inside the query with a DQL error. It is now the
SortDirection enum with the values ASC and DESC, and an invalid
value is rejected when the query is validated. This is a schema change.
Enum values are not quoted, and they are upper case:
# 13.x
filter: { name: { sort: "asc" } }
# 14.0
filter: { name: { sort: ASC } }
Variables still pass the value as a string, {"direction": "ASC"}, and are
declared with the SortDirection type. Lower case asc and desc,
which were accepted before, are rejected.
If you use more than one driver in a schema, share the TypeContainer
between them as described in tips, as you do for PageInfo.
Filter\Filters::type() now takes the TypeContainer as a second
parameter, from which the shared SortDirection type is fetched.
Input types were named <type>_Input_<uniqid>, which changed on every
build. They are now named <type>_Input when no field lists are given and
<type>_Input_<hash of the fields> otherwise, so they are the same on every
build. Calling input() again with the same entity and fields returns the
same type instead of a new one.
input() takes a new optional fourth parameter to name the type yourself,
for example $driver->input(Artist::class, ['name'], [], 'CreateArtistInput').
Regenerate any client types built from the schema.
$driver->input() previously ignored a name in the required or optional
list which was not a field of the entity, such as a typo or an association
name. It now throws ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Input,
suggesting the closest exposed field. Naming a field which is not exposed in
the driver’s group also throws; it previously failed with a TypeError.
The exception is thrown when the input type is first used, normally while the
schema is built. Remove any names it reports from your input() calls.
The globalEnable and ignoreFields config options are removed, along
with ConfigBuilder::globalEnable(), ConfigBuilder::ignoreFields(),
ConfigBuilder::ignoreField(), Config::getGlobalEnable(),
Config::getIgnoreFields() and the Metadata\GlobalEnable class.
Passing either option to Config now throws
ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Configuration. Expose
entities, fields and associations with the #[Entity], #[Field] and
#[Association] attributes instead.
Version 13.2 corrects the handling of the pagination arguments. Every
combination of first, last, before and after is now well
defined and no argument is discarded when another is present.
Cursors for index zero are no longer read as absent arguments
{ before: "<the first cursor>" } previously returned a full page. It now
returns an empty edges list, because no row precedes the first one.
Combined with last it previously returned the rows at the end of the
result set; it now returns nothing.
Arguments are no longer discarded
{ after: "...", before: "..." } previously ignored before and
{ last: n, after: "..." } previously ignored after. Both arguments now
narrow the range. { first: n, before: "..." } now returns the first n
rows before the cursor rather than the last n.
A zero count returns no rows
{ first: 0 } previously returned a full page. It now returns an empty
edges list.
Invalid arguments are reported
A negative first or last, or a cursor which cannot be decoded, was
previously coerced to something harmless and silently applied. It is now
reported to the client as a GraphQL error of type
ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Pagination.
A ``last`` larger than the result set no longer fails
{ last: 1000 } against a shorter collection previously produced
Offset must be a positive integer or zero from Doctrine. It now returns
every row.
PageInfo.startCursor and PageInfo.endCursor are now nullable String
rather than String!, matching the GraphQL Complete Connection Model. They
are null when edges is empty. Regenerate any client types built from the
schema. A client which only feeds the cursors back as after or before
needs no change.
Event\QueryBuilder::getOffset() and getLimit() are renamed to
getRequestedOffset() and getRequestedLimit().
The event is dispatched before the rows are counted so a listener can modify
the QueryBuilder, so these values are the window the client requested rather
than the window finally queried. A backward request - last without a
before cursor - reports an offset of zero.
Old (13.1):
function (QueryBuilder $event): void {
$offset = $event->getOffset();
$limit = $event->getLimit();
}
New (13.2):
function (QueryBuilder $event): void {
$offset = $event->getRequestedOffset();
$limit = $event->getRequestedLimit();
}
PaginationService¶The shared pagination service changed shape. Nothing else in the library calls it, but if you use it directly:
decodePaginationFields() returns int|null per field instead of int
so that an absent argument is distinct from index zero, and throws
Exception\Pagination on invalid input.
calculateOffsetAndLimit() takes int $itemCount as a required third
argument instead of an optional nullable one.
buildCursors() takes (int $offset, int $resultCount); the $itemCount
argument is gone and the returned array has start and end keys only.
buildPaginationResponse() takes a fourth argument, int $offset.
calculateRequestedOffsetAndLimit() is new and produces the values the
QueryBuilder event carries.
Version 13.x introduces significant performance improvements through QueryBuilder-based collection resolution, but requires migration of association event listeners.
Collection Event System Changed
The Criteria Event system for filtering associations has been replaced with QueryBuilder Events. This change provides:
83% faster query execution for filtered collections
90% reduction in memory usage
Database-level filtering with full index support
Single query execution instead of in-memory filtering
Migration Required
If you use criteriaEventName in your #[Association] attributes, rename
it to eventName and update your event listeners:
Old (12.x):
use ApiSkeletons\Doctrine\ORM\GraphQL\Event\Criteria;
#[GraphQL\Association(criteriaEventName: Artist::class . '.performances.criteria')]
public $performances;
$driver->get(EventDispatcher::class)->subscribeTo(
Artist::class . '.performances.criteria',
function (Criteria $event): void {
$event->getCriteria()->andWhere(
$event->getCriteria()->expr()->eq('venue', 'Delta Center')
);
}
);
New (13.x):
use ApiSkeletons\Doctrine\ORM\GraphQL\Event\QueryBuilder;
#[GraphQL\Association(eventName: Artist::class . '.performances')]
public $performances;
$driver->get(EventDispatcher::class)->subscribeTo(
Artist::class . '.performances',
function (QueryBuilder $event): void {
$event->getQueryBuilder()
->andWhere('entity.venue = :venue')
->setParameter('venue', 'Delta Center');
}
);
Migration Checklist:
Replace Event\Criteria with Event\QueryBuilder in use statements
Remove .criteria suffix from event names in attributes and listeners
Replace $event->getCriteria() with $event->getQueryBuilder()
Convert Criteria expressions to QueryBuilder syntax:
$criteria->expr()->eq('field', 'value') → 'entity.field = :param' + setParameter()
$criteria->expr()->gt('field', 10) → 'entity.field > :param' + setParameter()
$criteria->expr()->in('field', [1,2,3]) → 'entity.field IN (:param)' + setParameter()
Use entity as the default alias in WHERE clauses
After upgrading, you will automatically benefit from:
Faster collection queries (no performance tuning required)
Reduced memory consumption for large collections
Better database resource utilization
See the technical documentation for detailed performance benchmarks.
The $driver->connection() function no longer takes an ObjectType for an
entity. Instead, just pass the entity class name.
This repository, api-skeletons/doctrine-orm-graphql is a continuation of
api-skeletons/doctrine-graphql but there are some changes necessary to
move from the old repository to this new one.
The old namespace was ApiSkeletons\Doctrine\GraphQL and the new namespace
is ApiSkeletons\Doctrine\ORM\GraphQL. This is the only change between
the repositories that should affect you.
The namespace change was made to be more technically correct (the best kind of correct) as each repository only supports ORM and does not support ODM.
With the new repository the documentation was reviewed in whole and corrected where necessary. There is a new theme for the documentation, leaving the old ReadTheDocs default behind. And, though the documentation is still hosted by https://readthedocs.org it has been moved to a new domain: https://doctrine-orm-graphql.apiskeletons.dev
As a user of the old repository version 8.1.3, change your namespaces to the
new namespace then replace your composer require to api-skeletons/doctrine-orm-graphql ^8.1 and you will be upgraded to the new repository version 8.1.4.