From d9acf8ce4b2d25635610953a9468803ec33a4ea3 Mon Sep 17 00:00:00 2001 From: Tom H Anderson Date: Thu, 29 May 2025 14:20:49 -0600 Subject: [PATCH 01/18] Fix typo in docs --- docs/driver.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/driver.rst b/docs/driver.rst index 9f95085..d8b1367 100644 --- a/docs/driver.rst +++ b/docs/driver.rst @@ -15,7 +15,7 @@ Creating a Driver with all config options use ApiSkeletons\Doctrine\ORM\GraphQL\Driver; use ApiSkeletons\Doctrine\ORM\GraphQL\Filter\Filters; - $driver = new Driver($entityManager, new Config[ + $driver = new Driver($entityManager, new Config([ 'entityPrefix' => 'App\\ORM\\Entity\\', 'group' => 'customGroup', 'groupSuffix' => 'customGroupSuffix', From 41f89801d55d92d0a716b1a5dcdf403a03f4fc43 Mon Sep 17 00:00:00 2001 From: Tom H Anderson Date: Thu, 29 May 2025 14:24:50 -0600 Subject: [PATCH 02/18] Added sort priority --- src/Filter/Filters.php | 88 +++++++++++++++++++------------------ src/Filter/QueryBuilder.php | 63 ++++++++++++++++++++++++-- 2 files changed, 104 insertions(+), 47 deletions(-) diff --git a/src/Filter/Filters.php b/src/Filter/Filters.php index efdbf9a..fb9717a 100644 --- a/src/Filter/Filters.php +++ b/src/Filter/Filters.php @@ -17,20 +17,21 @@ */ enum Filters: string { - case EQ = 'eq'; - case NEQ = 'neq'; - case LT = 'lt'; - case LTE = 'lte'; - case GT = 'gt'; - case GTE = 'gte'; - case BETWEEN = 'between'; - case CONTAINS = 'contains'; - case STARTSWITH = 'startswith'; - case ENDSWITH = 'endswith'; - case IN = 'in'; - case NOTIN = 'notin'; - case ISNULL = 'isnull'; - case SORT = 'sort'; + case EQ = 'eq'; + case NEQ = 'neq'; + case LT = 'lt'; + case LTE = 'lte'; + case GT = 'gt'; + case GTE = 'gte'; + case BETWEEN = 'between'; + case CONTAINS = 'contains'; + case STARTSWITH = 'startswith'; + case ENDSWITH = 'endswith'; + case IN = 'in'; + case NOTIN = 'notin'; + case ISNULL = 'isnull'; + case SORT = 'sort'; + case SORTPRIORITY = 'sortPriority'; /** * Fetch the description for the filter @@ -38,21 +39,21 @@ enum Filters: string public function description(): string { return match ($this) { - self::EQ => 'Equals', - self::NEQ => 'Not equals', - self::LT => 'Less than', - self::LTE => 'Less than or equals', - self::GT => 'Greater than', - self::GTE => 'Greater than or equals', - self::BETWEEN => 'Is between from and to inclusive of from and to', - self::CONTAINS => 'Contains the value. Strings only.', - self::STARTSWITH => 'Starts with the value. Strings only.', - self::ENDSWITH => 'Ends with the value. Strings only.', - self::IN => 'In the array of values', - self::NOTIN => 'Not in the array of values', - self::ISNULL => 'Is null', - self::SORT => 'Sort by field. ASC or DESC.', - }; + self::EQ => 'Equals', + self::NEQ => 'Not equals', + self::LT => 'Less than', + self::LTE => 'Less than or equals', + self::GT => 'Greater than', + self::GTE => 'Greater than or equals', + self::BETWEEN => 'Is between from and to inclusive of from and to', + self::CONTAINS => 'Contains the value. Strings only.', + self::STARTSWITH => 'Starts with the value. Strings only.', + self::ENDSWITH => 'Ends with the value. Strings only.', + self::IN => 'In the array of values', + self::NOTIN => 'Not in the array of values', + self::ISNULL => 'Is null', + self::SORT => 'Sort by field. ASC or DESC.', + self::SORTPRIORITY => 'Specify the sort priority of a field. Priorities are sorted lowest number first. Sort must also be speciifed.',}; } /** @@ -61,20 +62,21 @@ public function description(): string public function type(ScalarType|ListOfType $type): Type { return match ($this) { - self::EQ => $type, - self::NEQ => $type, - self::LT => $type, - self::LTE => $type, - self::GT => $type, - self::GTE => $type, - self::BETWEEN => new Between($type), - self::CONTAINS => $type, - self::STARTSWITH => $type, - self::ENDSWITH => $type, - self::IN => Type::listOf($type), - self::NOTIN => Type::listOf($type), - self::ISNULL => Type::boolean(), - self::SORT => Type::string(), + self::EQ => $type, + self::NEQ => $type, + self::LT => $type, + self::LTE => $type, + self::GT => $type, + self::GTE => $type, + self::BETWEEN => new Between($type), + self::CONTAINS => $type, + self::STARTSWITH => $type, + self::ENDSWITH => $type, + self::IN => Type::listOf($type), + self::NOTIN => Type::listOf($type), + self::ISNULL => Type::boolean(), + self::SORT => Type::string(), + self::SORTPRIORITY => Type::int(), }; } diff --git a/src/Filter/QueryBuilder.php b/src/Filter/QueryBuilder.php index e36a83f..d212732 100644 --- a/src/Filter/QueryBuilder.php +++ b/src/Filter/QueryBuilder.php @@ -7,6 +7,7 @@ use ApiSkeletons\Doctrine\ORM\GraphQL\Type\Entity\Entity; use Doctrine\ORM\QueryBuilder as DoctrineQueryBuilder; +use GraphQL\Error\Error; use function array_flip; use function method_exists; use function uniqid; @@ -17,6 +18,8 @@ */ class QueryBuilder { + private array $sortFields = []; + /** * Add where clauses to a QueryBuilder based on the FilterType of the entity * @@ -42,6 +45,8 @@ public function apply( } } } + + $this->applySort($queryBuilder); } /** @@ -84,7 +89,7 @@ protected function contains(string $field, string $value, DoctrineQueryBuilder $ ->setParameter($parameter, '%' . $value . '%'); } - public function startsWith(string $field, string $value, DoctrineQueryBuilder $queryBuilder): void + protected function startsWith(string $field, string $value, DoctrineQueryBuilder $queryBuilder): void { $parameter = 'p' . uniqid(); $queryBuilder @@ -94,7 +99,7 @@ public function startsWith(string $field, string $value, DoctrineQueryBuilder $q ->setParameter($parameter, $value . '%'); } - public function endsWith(string $field, string $value, DoctrineQueryBuilder $queryBuilder): void + protected function endsWith(string $field, string $value, DoctrineQueryBuilder $queryBuilder): void { $parameter = 'p' . uniqid(); $queryBuilder @@ -104,7 +109,7 @@ public function endsWith(string $field, string $value, DoctrineQueryBuilder $que ->setParameter($parameter, '%' . $value); } - public function isnull(string $field, bool $value, DoctrineQueryBuilder $queryBuilder): void + protected function isnull(string $field, bool $value, DoctrineQueryBuilder $queryBuilder): void { if ($value === true) { $queryBuilder->andWhere( @@ -119,6 +124,56 @@ public function isnull(string $field, bool $value, DoctrineQueryBuilder $queryBu protected function sort(string $field, string $direction, DoctrineQueryBuilder $queryBuilder): void { - $queryBuilder->addOrderBy($field, $direction); + if (! isset($this->sortFields[$field])) { + $this->sortFields[$field] = []; + } + + // This method is used to set the sort direction for a field + // It will be used to apply sorting later in the applySort method + $this->sortFields[$field]['direction'] = $direction; + } + + protected function sortPriority(string $field, int $priority, DoctrineQueryBuilder $queryBuilder): void + { + if (! isset($this->sortFields[$field])) { + $this->sortFields[$field] = []; + } + + // This method is used to set the sort priority for a field + // It will be used to apply sorting later in the applySort method + $this->sortFields[$field]['priority'] = $priority; + } + + protected function applySort(DoctrineQueryBuilder $queryBuilder): void + { + // If no sort fields were added, do nothing + if (! $this->sortFields) { + return; + } + + // Sort fields by priority if set, otherwise by field name + uasort($this->sortFields, function ($a, $b) { + if (isset($a['priority']) && isset($b['priority'])) { + return $a['priority'] <=> $b['priority']; + } + return strcmp(key($a), key($b)); + }); + + $sortStrings = []; + + foreach ($this->sortFields as $field => $sort) { + // If the direction is not set, default to 'ASC' + if (! isset($sort['direction'])) { + throw new Error( + "Sort direction for field '$field' is not set but a sortPriority was. " + . "Please use the 'sort' filter to set the direction." + ); + } + + $sortStrings[] = "$field " . $sort['direction']; + } + + $sortString = implode(', ', $sortStrings); + $queryBuilder->addOrderBy($sortString); } } From 7e5837390c37cb2b3faee752fdd294115fdf5265 Mon Sep 17 00:00:00 2001 From: Tom H Anderson Date: Tue, 19 Aug 2025 01:16:02 -0600 Subject: [PATCH 03/18] Passing composer tests but not unittests --- src/Container.php | 3 + src/Event/Criteria.php | 2 + src/Event/EntityDefinition.php | 2 + src/Event/Metadata.php | 2 + src/Event/QueryBuilder.php | 2 + src/Filter/Filters.php | 3 +- src/Filter/QueryBuilder.php | 63 ++++++++++++++++---- src/Hydrator/HydratorContainer.php | 2 + src/Hydrator/Strategy/AssociationDefault.php | 3 + src/Hydrator/Strategy/Collection.php | 8 +++ src/Hydrator/Strategy/FieldDefault.php | 3 + src/Hydrator/Strategy/ToBoolean.php | 3 + src/Hydrator/Strategy/ToFloat.php | 3 + src/Hydrator/Strategy/ToInteger.php | 3 + src/Metadata/GlobalEnable.php | 2 + src/Metadata/MetadataFactory.php | 2 + src/Type/Blob.php | 4 ++ src/Type/Date.php | 4 ++ src/Type/DateImmutable.php | 4 ++ src/Type/DateTime.php | 4 ++ src/Type/DateTimeImmutable.php | 4 ++ src/Type/DateTimeTZ.php | 4 ++ src/Type/DateTimeTZImmutable.php | 4 ++ src/Type/Entity/EntityTypeContainer.php | 3 + src/Type/Json.php | 6 +- src/Type/Time.php | 4 ++ src/Type/TimeImmutable.php | 4 ++ 27 files changed, 139 insertions(+), 12 deletions(-) diff --git a/src/Container.php b/src/Container.php index 49be4cb..8848527 100644 --- a/src/Container.php +++ b/src/Container.php @@ -6,6 +6,7 @@ use Closure; use GraphQL\Error\Error; +use Override; use Psr\Container\ContainerInterface; use ReflectionClass; use ReflectionException; @@ -21,12 +22,14 @@ abstract class Container implements ContainerInterface /** @var mixed[] */ protected array $register = []; + #[Override] public function has(string $id): bool { return isset($this->register[strtolower($id)]); } /** @throws Error */ + #[Override] public function get(string $id): mixed { $id = strtolower($id); diff --git a/src/Event/Criteria.php b/src/Event/Criteria.php index 08dfde4..58dec4d 100644 --- a/src/Event/Criteria.php +++ b/src/Event/Criteria.php @@ -9,6 +9,7 @@ use Doctrine\ORM\PersistentCollection; use GraphQL\Type\Definition\ResolveInfo; use League\Event\HasEventName; +use Override; /** * This event is dispatched when a Doctrine Criteria is created. @@ -34,6 +35,7 @@ public function __construct( ) { } + #[Override] public function eventName(): string { return $this->eventName; diff --git a/src/Event/EntityDefinition.php b/src/Event/EntityDefinition.php index 466b8ec..a4519e5 100644 --- a/src/Event/EntityDefinition.php +++ b/src/Event/EntityDefinition.php @@ -6,6 +6,7 @@ use ArrayObject; use League\Event\HasEventName; +use Override; /** * This event is fired each time an entity GraphQL type is created @@ -20,6 +21,7 @@ public function __construct( ) { } + #[Override] public function eventName(): string { return $this->eventName; diff --git a/src/Event/Metadata.php b/src/Event/Metadata.php index a533818..543564c 100644 --- a/src/Event/Metadata.php +++ b/src/Event/Metadata.php @@ -6,6 +6,7 @@ use ArrayObject; use League\Event\HasEventName; +use Override; /** * This event is fired when the metadta is created @@ -19,6 +20,7 @@ public function __construct( ) { } + #[Override] public function eventName(): string { return $this->eventName; diff --git a/src/Event/QueryBuilder.php b/src/Event/QueryBuilder.php index dd11961..56daeba 100644 --- a/src/Event/QueryBuilder.php +++ b/src/Event/QueryBuilder.php @@ -7,6 +7,7 @@ use Doctrine\ORM\QueryBuilder as DoctrineQueryBuilder; use GraphQL\Type\Definition\ResolveInfo; use League\Event\HasEventName; +use Override; /** * This event is fired when the QueryBuilder is created for an entity @@ -27,6 +28,7 @@ public function __construct( ) { } + #[Override] public function eventName(): string { return $this->eventName; diff --git a/src/Filter/Filters.php b/src/Filter/Filters.php index fb9717a..38c5be9 100644 --- a/src/Filter/Filters.php +++ b/src/Filter/Filters.php @@ -53,7 +53,8 @@ public function description(): string self::NOTIN => 'Not in the array of values', self::ISNULL => 'Is null', self::SORT => 'Sort by field. ASC or DESC.', - self::SORTPRIORITY => 'Specify the sort priority of a field. Priorities are sorted lowest number first. Sort must also be speciifed.',}; + self::SORTPRIORITY => 'Specify the sort priority of a field. Priorities are sorted lowest number first. Sort must also be speciifed.', + }; } /** diff --git a/src/Filter/QueryBuilder.php b/src/Filter/QueryBuilder.php index d212732..5355f74 100644 --- a/src/Filter/QueryBuilder.php +++ b/src/Filter/QueryBuilder.php @@ -6,10 +6,13 @@ use ApiSkeletons\Doctrine\ORM\GraphQL\Type\Entity\Entity; use Doctrine\ORM\QueryBuilder as DoctrineQueryBuilder; - use GraphQL\Error\Error; + use function array_flip; -use function method_exists; +use function implode; +use function key; +use function strcmp; +use function uasort; use function uniqid; /** @@ -18,6 +21,7 @@ */ class QueryBuilder { + /** @var mixed[] */ private array $sortFields = []; /** @@ -38,10 +42,22 @@ public function apply( foreach ($filters as $filter => $value) { $filter = Filters::from($filter); - if (method_exists($this, $filter->value) === false) { - $this->default($filter, $queryBuilderField, $value, $queryBuilder); - } else { - $this->{$filter->value}($queryBuilderField, $value, $queryBuilder); + switch ($filter) { + case Filters::EQ: + case Filters::NEQ: + case Filters::GT: + case Filters::GTE: + case Filters::LT: + case Filters::LTE: + case Filters::IN: + case Filters::NOTIN: + case Filters::ISNULL: + // These filters are handled by the default method + $this->default($filter, $queryBuilderField, $value, $queryBuilder); + break; + default: + $this->{$filter->value}($queryBuilderField, $value, $queryBuilder); + break; } } } @@ -54,6 +70,30 @@ public function apply( */ protected function default(Filters $filter, string $field, mixed $value, DoctrineQueryBuilder $queryBuilder): void { + switch ($filter) { + case Filters::EQ: + case Filters::NEQ: + case Filters::GT: + case Filters::GTE: + case Filters::LT: + case Filters::LTE: + case Filters::IN: + case Filters::NOTIN: + break; + case Filters::ISNULL: + $parameter = 'p' . uniqid(); + $queryBuilder + ->andWhere( + $queryBuilder->expr()->{$filter->value}($field), + ) + ->setParameter($parameter, $value); + + return; + + default: + return; + } + $parameter = 'p' . uniqid(); $queryBuilder ->andWhere( @@ -152,10 +192,11 @@ protected function applySort(DoctrineQueryBuilder $queryBuilder): void } // Sort fields by priority if set, otherwise by field name - uasort($this->sortFields, function ($a, $b) { + uasort($this->sortFields, static function ($a, $b) { if (isset($a['priority']) && isset($b['priority'])) { return $a['priority'] <=> $b['priority']; } + return strcmp(key($a), key($b)); }); @@ -165,12 +206,14 @@ protected function applySort(DoctrineQueryBuilder $queryBuilder): void // If the direction is not set, default to 'ASC' if (! isset($sort['direction'])) { throw new Error( - "Sort direction for field '$field' is not set but a sortPriority was. " - . "Please use the 'sort' filter to set the direction." + "Sort direction for field '" + . $field + . "' is not set but a sortPriority was. " + . "Please use the 'sort' filter to set the direction.", ); } - $sortStrings[] = "$field " . $sort['direction']; + $sortStrings[] = $field . ' ' . $sort['direction']; } $sortString = implode(', ', $sortStrings); diff --git a/src/Hydrator/HydratorContainer.php b/src/Hydrator/HydratorContainer.php index 3cbe515..350a147 100644 --- a/src/Hydrator/HydratorContainer.php +++ b/src/Hydrator/HydratorContainer.php @@ -11,6 +11,7 @@ use GraphQL\Error\Error; use Laminas\Hydrator\NamingStrategy\MapNamingStrategy; use Laminas\Hydrator\Strategy\StrategyInterface; +use Override; use function assert; use function class_implements; @@ -36,6 +37,7 @@ public function __construct( } /** @throws Error */ + #[Override] public function get(string $id): mixed { if ($this->has($id)) { diff --git a/src/Hydrator/Strategy/AssociationDefault.php b/src/Hydrator/Strategy/AssociationDefault.php index bb2d49a..4b98a8e 100644 --- a/src/Hydrator/Strategy/AssociationDefault.php +++ b/src/Hydrator/Strategy/AssociationDefault.php @@ -5,6 +5,7 @@ namespace ApiSkeletons\Doctrine\ORM\GraphQL\Hydrator\Strategy; use Laminas\Hydrator\Strategy\StrategyInterface; +use Override; /** * Take no action on an association. This class exists to @@ -13,6 +14,7 @@ class AssociationDefault extends Collection implements StrategyInterface { + #[Override] public function extract(mixed $value, object|null $object = null): mixed { return $value; @@ -23,6 +25,7 @@ public function extract(mixed $value, object|null $object = null): mixed * * @codeCoverageIgnore */ + #[Override] public function hydrate(mixed $value, array|null $data): mixed { return $value; diff --git a/src/Hydrator/Strategy/Collection.php b/src/Hydrator/Strategy/Collection.php index 7662ba8..00c3745 100644 --- a/src/Hydrator/Strategy/Collection.php +++ b/src/Hydrator/Strategy/Collection.php @@ -12,6 +12,7 @@ use Doctrine\Persistence\Mapping\ClassMetadata; use InvalidArgumentException; use LogicException; +use Override; use ReflectionException; use function is_array; @@ -39,11 +40,13 @@ public function __construct(Inflector|null $inflector = null) $this->inflector = $inflector ?? InflectorFactory::create()->build(); } + #[Override] public function setCollectionName(string $collectionName): void { $this->collectionName = $collectionName; } + #[Override] public function getCollectionName(): string { if ($this->collectionName === null) { @@ -53,11 +56,13 @@ public function getCollectionName(): string return $this->collectionName; } + #[Override] public function setClassMetadata(ClassMetadata $classMetadata): void { $this->metadata = $classMetadata; } + #[Override] public function getClassMetadata(): ClassMetadata { if ($this->metadata === null) { @@ -67,11 +72,13 @@ public function getClassMetadata(): ClassMetadata return $this->metadata; } + #[Override] public function setObject(object $object): void { $this->object = $object; } + #[Override] public function getObject(): object { if ($this->object === null) { @@ -89,6 +96,7 @@ public function getObject(): object * * @return mixed Returns the value that should be extracted. */ + #[Override] public function extract(mixed $value, object|null $object = null): mixed { return $value; diff --git a/src/Hydrator/Strategy/FieldDefault.php b/src/Hydrator/Strategy/FieldDefault.php index 7d2eba7..3f223ee 100644 --- a/src/Hydrator/Strategy/FieldDefault.php +++ b/src/Hydrator/Strategy/FieldDefault.php @@ -5,6 +5,7 @@ namespace ApiSkeletons\Doctrine\ORM\GraphQL\Hydrator\Strategy; use Laminas\Hydrator\Strategy\StrategyInterface; +use Override; /** * Return the same value @@ -12,6 +13,7 @@ class FieldDefault extends Collection implements StrategyInterface { + #[Override] public function extract(mixed $value, object|null $object = null): mixed { return $value; @@ -22,6 +24,7 @@ public function extract(mixed $value, object|null $object = null): mixed * * @codeCoverageIgnore */ + #[Override] public function hydrate(mixed $value, array|null $data): mixed { return $value; diff --git a/src/Hydrator/Strategy/ToBoolean.php b/src/Hydrator/Strategy/ToBoolean.php index cb8b54d..dfec0ce 100644 --- a/src/Hydrator/Strategy/ToBoolean.php +++ b/src/Hydrator/Strategy/ToBoolean.php @@ -5,6 +5,7 @@ namespace ApiSkeletons\Doctrine\ORM\GraphQL\Hydrator\Strategy; use Laminas\Hydrator\Strategy\StrategyInterface; +use Override; /** * Transform a value into a php native boolean @@ -14,6 +15,7 @@ class ToBoolean extends Collection implements StrategyInterface { + #[Override] public function extract(mixed $value, object|null $object = null): bool|null { if ($value === null) { @@ -30,6 +32,7 @@ public function extract(mixed $value, object|null $object = null): bool|null * * @codeCoverageIgnore */ + #[Override] public function hydrate(mixed $value, array|null $data): bool|null { if ($value === null) { diff --git a/src/Hydrator/Strategy/ToFloat.php b/src/Hydrator/Strategy/ToFloat.php index 51f9953..6949fb0 100644 --- a/src/Hydrator/Strategy/ToFloat.php +++ b/src/Hydrator/Strategy/ToFloat.php @@ -5,6 +5,7 @@ namespace ApiSkeletons\Doctrine\ORM\GraphQL\Hydrator\Strategy; use Laminas\Hydrator\Strategy\StrategyInterface; +use Override; use function floatval; @@ -16,6 +17,7 @@ class ToFloat extends Collection implements StrategyInterface { + #[Override] public function extract(mixed $value, object|null $object = null): mixed { if ($value === null) { @@ -32,6 +34,7 @@ public function extract(mixed $value, object|null $object = null): mixed * * @codeCoverageIgnore */ + #[Override] public function hydrate(mixed $value, array|null $data): mixed { if ($value === null) { diff --git a/src/Hydrator/Strategy/ToInteger.php b/src/Hydrator/Strategy/ToInteger.php index f1e2e09..3144643 100644 --- a/src/Hydrator/Strategy/ToInteger.php +++ b/src/Hydrator/Strategy/ToInteger.php @@ -5,6 +5,7 @@ namespace ApiSkeletons\Doctrine\ORM\GraphQL\Hydrator\Strategy; use Laminas\Hydrator\Strategy\StrategyInterface; +use Override; use function intval; @@ -16,6 +17,7 @@ class ToInteger extends Collection implements StrategyInterface { + #[Override] public function extract(mixed $value, object|null $object = null): mixed { if ($value === null) { @@ -32,6 +34,7 @@ public function extract(mixed $value, object|null $object = null): mixed * * @codeCoverageIgnore */ + #[Override] public function hydrate(mixed $value, array|null $data): mixed { if ($value === null) { diff --git a/src/Metadata/GlobalEnable.php b/src/Metadata/GlobalEnable.php index 63a367b..e89e9cb 100644 --- a/src/Metadata/GlobalEnable.php +++ b/src/Metadata/GlobalEnable.php @@ -11,6 +11,7 @@ use ArrayObject; use Doctrine\ORM\EntityManager; use League\Event\EventDispatcher; +use Override; use function in_array; @@ -97,6 +98,7 @@ private function buildAssociationMetadata(string $entityClass): void } } + #[Override] protected function getConfig(): Config { return $this->config; diff --git a/src/Metadata/MetadataFactory.php b/src/Metadata/MetadataFactory.php index 461dcbb..d02cb3e 100644 --- a/src/Metadata/MetadataFactory.php +++ b/src/Metadata/MetadataFactory.php @@ -14,6 +14,7 @@ use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\ClassMetadata; use League\Event\EventDispatcher; +use Override; use ReflectionClass; use function assert; @@ -205,6 +206,7 @@ private function buildMetadataForAssociations( } } + #[Override] protected function getConfig(): Config { return $this->config; diff --git a/src/Type/Blob.php b/src/Type/Blob.php index 60988f0..fe0f307 100644 --- a/src/Type/Blob.php +++ b/src/Type/Blob.php @@ -8,6 +8,7 @@ use GraphQL\Language\AST\Node as ASTNode; use GraphQL\Language\AST\StringValueNode; use GraphQL\Type\Definition\ScalarType; +use Override; use function base64_decode; use function base64_encode; @@ -22,6 +23,7 @@ class Blob extends ScalarType { public string|null $description = 'A binary file base64 encoded.'; + #[Override] public function parseLiteral(ASTNode $valueNode, array|null $variables = null): string { // @codeCoverageIgnoreStart @@ -34,6 +36,7 @@ public function parseLiteral(ASTNode $valueNode, array|null $variables = null): return $this->parseValue($valueNode->value); } + #[Override] public function parseValue(mixed $value): mixed { if (! is_string($value)) { @@ -49,6 +52,7 @@ public function parseValue(mixed $value): mixed return $data; } + #[Override] public function serialize(mixed $value): mixed { if (! $value) { diff --git a/src/Type/Date.php b/src/Type/Date.php index a4d2810..00ccf3f 100644 --- a/src/Type/Date.php +++ b/src/Type/Date.php @@ -9,6 +9,7 @@ use GraphQL\Language\AST\Node as ASTNode; use GraphQL\Language\AST\StringValueNode; use GraphQL\Type\Definition\ScalarType; +use Override; use function is_string; use function preg_match; @@ -21,6 +22,7 @@ class Date extends ScalarType public string|null $description = 'The `Date` scalar type represents datetime data.' . 'The format is e.g. 2004-02-12.'; + #[Override] public function parseLiteral(ASTNode $valueNode, array|null $variables = null): DateTime|null { // @codeCoverageIgnoreStart @@ -33,6 +35,7 @@ public function parseLiteral(ASTNode $valueNode, array|null $variables = null): return $this->parseValue($valueNode->value); } + #[Override] public function parseValue(mixed $value): DateTime { if (! is_string($value)) { @@ -46,6 +49,7 @@ public function parseValue(mixed $value): DateTime return DateTime::createFromFormat(DateTime::ATOM, $value . 'T00:00:00+00:00'); } + #[Override] public function serialize(mixed $value): string|null { if (is_string($value)) { diff --git a/src/Type/DateImmutable.php b/src/Type/DateImmutable.php index eb1dcd3..d25851c 100644 --- a/src/Type/DateImmutable.php +++ b/src/Type/DateImmutable.php @@ -9,6 +9,7 @@ use GraphQL\Language\AST\Node as ASTNode; use GraphQL\Language\AST\StringValueNode; use GraphQL\Type\Definition\ScalarType; +use Override; use function is_string; use function preg_match; @@ -21,6 +22,7 @@ class DateImmutable extends ScalarType public string|null $description = 'The `date_immutable` scalar type represents datetime data.' . 'The format is e.g. 2004-02-12.'; + #[Override] public function parseLiteral(ASTNode $valueNode, array|null $variables = null): DateTimeImmutable|false { // @codeCoverageIgnoreStart @@ -33,6 +35,7 @@ public function parseLiteral(ASTNode $valueNode, array|null $variables = null): return $this->parseValue($valueNode->value); } + #[Override] public function parseValue(mixed $value): DateTimeImmutable|false { if (! is_string($value)) { @@ -46,6 +49,7 @@ public function parseValue(mixed $value): DateTimeImmutable|false return DateTimeImmutable::createFromFormat(DateTimeImmutable::ATOM, $value . 'T00:00:00+00:00'); } + #[Override] public function serialize(mixed $value): string|null { if (is_string($value)) { diff --git a/src/Type/DateTime.php b/src/Type/DateTime.php index 21cc782..6daff48 100644 --- a/src/Type/DateTime.php +++ b/src/Type/DateTime.php @@ -9,6 +9,7 @@ use GraphQL\Language\AST\Node as ASTNode; use GraphQL\Language\AST\StringValueNode; use GraphQL\Type\Definition\ScalarType; +use Override; use function is_string; @@ -21,6 +22,7 @@ class DateTime extends ScalarType public string|null $description = 'The `datetime` scalar type represents datetime data.' . 'The format is ISO-8601 e.g. 2004-02-12T15:19:21+00:00.'; + #[Override] public function parseLiteral(ASTNode $valueNode, array|null $variables = null): PHPDateTime { // @codeCoverageIgnoreStart @@ -33,6 +35,7 @@ public function parseLiteral(ASTNode $valueNode, array|null $variables = null): return $this->parseValue($valueNode->value); } + #[Override] public function parseValue(mixed $value): PHPDateTime { if (! is_string($value)) { @@ -48,6 +51,7 @@ public function parseValue(mixed $value): PHPDateTime return $data; } + #[Override] public function serialize(mixed $value): string|null { if ($value instanceof PHPDateTime) { diff --git a/src/Type/DateTimeImmutable.php b/src/Type/DateTimeImmutable.php index c34d9e4..aeaad76 100644 --- a/src/Type/DateTimeImmutable.php +++ b/src/Type/DateTimeImmutable.php @@ -9,6 +9,7 @@ use GraphQL\Language\AST\Node as ASTNode; use GraphQL\Language\AST\StringValueNode; use GraphQL\Type\Definition\ScalarType; +use Override; use function is_string; @@ -21,6 +22,7 @@ class DateTimeImmutable extends ScalarType public string|null $description = 'The `datetime_immutable` scalar type represents datetime data.' . 'The format is ISO-8601 e.g. 2004-02-12T15:19:21+00:00'; + #[Override] public function parseLiteral(ASTNode $valueNode, array|null $variables = null): PHPDateTimeImmutable { // @codeCoverageIgnoreStart @@ -33,6 +35,7 @@ public function parseLiteral(ASTNode $valueNode, array|null $variables = null): return $this->parseValue($valueNode->value); } + #[Override] public function parseValue(mixed $value): PHPDateTimeImmutable { if (! is_string($value)) { @@ -48,6 +51,7 @@ public function parseValue(mixed $value): PHPDateTimeImmutable return $data; } + #[Override] public function serialize(mixed $value): string|null { if ($value instanceof PHPDateTimeImmutable) { diff --git a/src/Type/DateTimeTZ.php b/src/Type/DateTimeTZ.php index d628168..de5ac5c 100644 --- a/src/Type/DateTimeTZ.php +++ b/src/Type/DateTimeTZ.php @@ -9,6 +9,7 @@ use GraphQL\Language\AST\Node as ASTNode; use GraphQL\Language\AST\StringValueNode; use GraphQL\Type\Definition\ScalarType; +use Override; use function is_string; @@ -21,6 +22,7 @@ class DateTimeTZ extends ScalarType public string|null $description = 'The `datetimetz` scalar type represents datetime data.' . 'The format is ISO-8601 e.g. 2004-02-12T15:19:21+00:00.'; + #[Override] public function parseLiteral(ASTNode $valueNode, array|null $variables = null): PHPDateTimeTZ { // @codeCoverageIgnoreStart @@ -33,6 +35,7 @@ public function parseLiteral(ASTNode $valueNode, array|null $variables = null): return $this->parseValue($valueNode->value); } + #[Override] public function parseValue(mixed $value): PHPDateTimeTZ { if (! is_string($value)) { @@ -48,6 +51,7 @@ public function parseValue(mixed $value): PHPDateTimeTZ return $data; } + #[Override] public function serialize(mixed $value): string|null { if ($value instanceof PHPDateTimeTZ) { diff --git a/src/Type/DateTimeTZImmutable.php b/src/Type/DateTimeTZImmutable.php index 0858eac..0ccd638 100644 --- a/src/Type/DateTimeTZImmutable.php +++ b/src/Type/DateTimeTZImmutable.php @@ -9,6 +9,7 @@ use GraphQL\Language\AST\Node as ASTNode; use GraphQL\Language\AST\StringValueNode; use GraphQL\Type\Definition\ScalarType; +use Override; use function is_string; @@ -21,6 +22,7 @@ class DateTimeTZImmutable extends ScalarType public string|null $description = 'The `datetimetz_immutable` scalar type represents datetime data.' . 'The format is ISO-8601 e.g. 2004-02-12T15:19:21+00:00'; + #[Override] public function parseLiteral(ASTNode $valueNode, array|null $variables = null): PHPDateTimeTZImmutable { // @codeCoverageIgnoreStart @@ -33,6 +35,7 @@ public function parseLiteral(ASTNode $valueNode, array|null $variables = null): return $this->parseValue($valueNode->value); } + #[Override] public function parseValue(mixed $value): PHPDateTimeTZImmutable { if (! is_string($value)) { @@ -48,6 +51,7 @@ public function parseValue(mixed $value): PHPDateTimeTZImmutable return $data; } + #[Override] public function serialize(mixed $value): string|null { if ($value instanceof PHPDateTimeTZImmutable) { diff --git a/src/Type/Entity/EntityTypeContainer.php b/src/Type/Entity/EntityTypeContainer.php index 6305056..1310f6d 100644 --- a/src/Type/Entity/EntityTypeContainer.php +++ b/src/Type/Entity/EntityTypeContainer.php @@ -6,6 +6,7 @@ use ApiSkeletons\Doctrine\ORM\GraphQL\Container; use ApiSkeletons\Doctrine\ORM\GraphQL\Driver; +use Override; use function assert; use function strtolower; @@ -25,6 +26,7 @@ public function __construct( /** * Use the metadata to determine if the entity is available */ + #[Override] public function has(string $id): bool { return isset($this->container->get('metadata')[$id]); @@ -33,6 +35,7 @@ public function has(string $id): bool /** * Create and return an Entity object */ + #[Override] public function get(string $id, string|null $eventName = null): mixed { // Allow for entities with a custom eventName diff --git a/src/Type/Json.php b/src/Type/Json.php index 44b6318..32b678d 100644 --- a/src/Type/Json.php +++ b/src/Type/Json.php @@ -8,6 +8,7 @@ use GraphQL\Language\AST\Node as ASTNode; use GraphQL\Language\AST\StringValueNode; use GraphQL\Type\Definition\ScalarType; +use Override; use function is_string; use function json_decode; @@ -21,6 +22,7 @@ class Json extends ScalarType // phpcs:disable SlevomatCodingStandard.TypeHints.PropertyTypeHint.MissingAnyTypeHint public string|null $description = 'The `json` scalar type represents json data.'; + #[Override] public function parseLiteral(ASTNode $valueNode, array|null $variables = null): string { // @codeCoverageIgnoreStart @@ -37,6 +39,7 @@ public function parseLiteral(ASTNode $valueNode, array|null $variables = null): * * @throws Error */ + #[Override] public function parseValue(mixed $value): array|null { if (! is_string($value)) { @@ -52,7 +55,8 @@ public function parseValue(mixed $value): array|null return $data; } - public function serialize(mixed $value): string|null + #[Override] + public function serialize(mixed $value): false|string { return json_encode($value); } diff --git a/src/Type/Time.php b/src/Type/Time.php index 644c64f..62a9c8d 100644 --- a/src/Type/Time.php +++ b/src/Type/Time.php @@ -9,6 +9,7 @@ use GraphQL\Language\AST\Node as ASTNode; use GraphQL\Language\AST\StringValueNode; use GraphQL\Type\Definition\ScalarType; +use Override; use function is_string; use function preg_match; @@ -22,6 +23,7 @@ class Time extends ScalarType public string|null $description = 'The `Time` scalar type represents time data.' . 'The format is e.g. 24 hour:minutes:seconds.microseconds'; + #[Override] public function parseLiteral(ASTNode $valueNode, array|null $variables = null): string|null { // @codeCoverageIgnoreStart @@ -37,6 +39,7 @@ public function parseLiteral(ASTNode $valueNode, array|null $variables = null): /** * Parse H:i:s.u and H:i:s */ + #[Override] public function parseValue(mixed $value): PHPDateTime { if (! is_string($value)) { @@ -55,6 +58,7 @@ public function parseValue(mixed $value): PHPDateTime return PHPDateTime::createFromFormat('H:i:s.u', $value); } + #[Override] public function serialize(mixed $value): string|null { if ($value instanceof PHPDateTime) { diff --git a/src/Type/TimeImmutable.php b/src/Type/TimeImmutable.php index bf4e50d..1cc747a 100644 --- a/src/Type/TimeImmutable.php +++ b/src/Type/TimeImmutable.php @@ -9,6 +9,7 @@ use GraphQL\Language\AST\Node as ASTNode; use GraphQL\Language\AST\StringValueNode; use GraphQL\Type\Definition\ScalarType; +use Override; use function is_string; use function preg_match; @@ -22,6 +23,7 @@ class TimeImmutable extends ScalarType public string|null $description = 'The `Time` scalar type represents time data.' . 'The format is e.g. 24 hour:minutes:seconds'; + #[Override] public function parseLiteral(ASTNode $valueNode, array|null $variables = null): string { // @codeCoverageIgnoreStart @@ -34,6 +36,7 @@ public function parseLiteral(ASTNode $valueNode, array|null $variables = null): return $valueNode->value; } + #[Override] public function parseValue(mixed $value): PHPDateTime|false { if (! is_string($value)) { @@ -52,6 +55,7 @@ public function parseValue(mixed $value): PHPDateTime|false return PHPDateTime::createFromFormat('H:i:s.u', $value); } + #[Override] public function serialize(mixed $value): string|null { if ($value instanceof PHPDateTime) { From 60091d518dbbbfce8d3c68228bd7017c95465ca2 Mon Sep 17 00:00:00 2001 From: Tom H Anderson Date: Tue, 19 Aug 2025 13:53:32 -0600 Subject: [PATCH 04/18] PHP 8.3 --- composer.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 450d6f1..e86e10b 100644 --- a/composer.json +++ b/composer.json @@ -10,18 +10,18 @@ } ], "require": { - "php": "^8.1", - "doctrine/orm": "^2.18 || ^3.0", + "php": "^8.3", + "doctrine/orm": "^3.0", "doctrine/doctrine-laminas-hydrator": "^3.2", "webonyx/graphql-php": "^v15.0", - "psr/container": "^1.1 || ^2.0", + "psr/container": "^2.0", "league/event": "^3.0" }, "require-dev": { - "doctrine/coding-standard": "^11.0 || ^12.0", + "doctrine/coding-standard": "^13.0", "doctrine/dbal": "^3.1 || ^4.0", "phpunit/phpunit": "^9.6", - "vimeo/psalm": "^5.4", + "vimeo/psalm": "^6.13", "symfony/cache": "^5.3||^6.2", "php-parallel-lint/php-parallel-lint": "^1.3.2", "phpstan/phpstan": "^1.12 || ^2.0" From 7a80b4146d5abef4dbb71feff521453e419ffe63 Mon Sep 17 00:00:00 2001 From: Tom H Anderson Date: Tue, 19 Aug 2025 14:44:26 -0600 Subject: [PATCH 05/18] Unit tests passing; sortPriority not tested --- src/Filter/QueryBuilder.php | 29 ++++++++++++------- .../Filter/ConfigExcludeFiltersTest.php | 8 ++--- test/Feature/Filter/ExcludeFiltersTest.php | 8 ++--- test/Feature/Resolve/EntityFilterTest.php | 6 ++-- 4 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/Filter/QueryBuilder.php b/src/Filter/QueryBuilder.php index 5355f74..ebd2add 100644 --- a/src/Filter/QueryBuilder.php +++ b/src/Filter/QueryBuilder.php @@ -11,7 +11,9 @@ use function array_flip; use function implode; use function key; +use function print_r; use function strcmp; +use function strtoupper; use function uasort; use function uniqid; @@ -70,6 +72,9 @@ public function apply( */ protected function default(Filters $filter, string $field, mixed $value, DoctrineQueryBuilder $queryBuilder): void { + /** + * psalm errors without proper filtering here + */ switch ($filter) { case Filters::EQ: case Filters::NEQ: @@ -81,12 +86,17 @@ protected function default(Filters $filter, string $field, mixed $value, Doctrin case Filters::NOTIN: break; case Filters::ISNULL: - $parameter = 'p' . uniqid(); - $queryBuilder - ->andWhere( - $queryBuilder->expr()->{$filter->value}($field), - ) - ->setParameter($parameter, $value); + if ($value) { + $queryBuilder + ->andWhere( + $queryBuilder->expr()->isNull($field), + ); + } else { + $queryBuilder + ->andWhere( + $queryBuilder->expr()->isNotNull($field), + ); + } return; @@ -170,7 +180,7 @@ protected function sort(string $field, string $direction, DoctrineQueryBuilder $ // This method is used to set the sort direction for a field // It will be used to apply sorting later in the applySort method - $this->sortFields[$field]['direction'] = $direction; + $this->sortFields[$field]['direction'] = strtoupper($direction); } protected function sortPriority(string $field, int $priority, DoctrineQueryBuilder $queryBuilder): void @@ -213,10 +223,7 @@ protected function applySort(DoctrineQueryBuilder $queryBuilder): void ); } - $sortStrings[] = $field . ' ' . $sort['direction']; + $queryBuilder->addOrderBy($field, $sort['direction']); } - - $sortString = implode(', ', $sortStrings); - $queryBuilder->addOrderBy($sortString); } } diff --git a/test/Feature/Filter/ConfigExcludeFiltersTest.php b/test/Feature/Filter/ConfigExcludeFiltersTest.php index 092877e..539be2d 100644 --- a/test/Feature/Filter/ConfigExcludeFiltersTest.php +++ b/test/Feature/Filter/ConfigExcludeFiltersTest.php @@ -47,28 +47,28 @@ public function testConfigExcludeFilters(): void $result = GraphQL::executeQuery($schema, $query); foreach ($result->errors as $error) { - $this->assertEquals('Field "eq" is not defined by type "Filters_String_0812311810b0ba1d34247150620b78b0".', $error->getMessage()); + $this->assertEquals('Field "eq" is not defined by type "Filters_String_2fcc46c308f783c42451d0c9ee076e5b".', $error->getMessage()); } $query = '{ artists (filter: { name: { neq: "Grateful Dead" } } ) { edges { node { name } } } }'; $result = GraphQL::executeQuery($schema, $query); foreach ($result->errors as $error) { - $this->assertEquals('Field "neq" is not defined by type "Filters_String_0812311810b0ba1d34247150620b78b0".', $error->getMessage()); + $this->assertEquals('Field "neq" is not defined by type "Filters_String_2fcc46c308f783c42451d0c9ee076e5b".', $error->getMessage()); } $query = '{ artists { edges { node { performances ( filter: {venue: { neq: "test"} } ) { edges { node { venue } } } } } } }'; $result = GraphQL::executeQuery($schema, $query); foreach ($result->errors as $error) { - $this->assertEquals('Field "neq" is not defined by type "Filters_String_0812311810b0ba1d34247150620b78b0".', $error->getMessage()); + $this->assertEquals('Field "neq" is not defined by type "Filters_String_2fcc46c308f783c42451d0c9ee076e5b".', $error->getMessage()); } $query = '{ artists { edges { node { performances ( filter: {venue: { contains: "test" } } ) { edges { node { venue } } } } } } }'; $result = GraphQL::executeQuery($schema, $query); foreach ($result->errors as $error) { - $this->assertEquals('Field "contains" is not defined by type "Filters_String_0812311810b0ba1d34247150620b78b0". Did you mean "notin"?', $error->getMessage()); + $this->assertEquals('Field "contains" is not defined by type "Filters_String_2fcc46c308f783c42451d0c9ee076e5b". Did you mean "notin"?', $error->getMessage()); } } } diff --git a/test/Feature/Filter/ExcludeFiltersTest.php b/test/Feature/Filter/ExcludeFiltersTest.php index 0e2e929..dd21176 100644 --- a/test/Feature/Filter/ExcludeFiltersTest.php +++ b/test/Feature/Filter/ExcludeFiltersTest.php @@ -39,28 +39,28 @@ public function testExcludeCriteria(): void $result = GraphQL::executeQuery($schema, $query); foreach ($result->errors as $error) { - $this->assertEquals('Field "eq" is not defined by type "Filters_String_a03586330c4e7326edac556450d913ee".', $error->getMessage()); + $this->assertEquals('Field "eq" is not defined by type "Filters_String_d85f45158139644c1511e7f9d22d6068".', $error->getMessage()); } $query = '{ artists (filter: { name: { neq: "Grateful Dead" } } ) { edges { node { name } } } }'; $result = GraphQL::executeQuery($schema, $query); foreach ($result->errors as $error) { - $this->assertEquals('Field "neq" is not defined by type "Filters_String_a03586330c4e7326edac556450d913ee".', $error->getMessage()); + $this->assertEquals('Field "neq" is not defined by type "Filters_String_d85f45158139644c1511e7f9d22d6068".', $error->getMessage()); } $query = '{ artists { edges { node { performances ( filter: {venue: { neq: "test"} } ) { edges { node { venue } } } } } } }'; $result = GraphQL::executeQuery($schema, $query); foreach ($result->errors as $error) { - $this->assertEquals('Field "neq" is not defined by type "Filters_String_e55a7b533af3c46236f06d0fb99f08c6". Did you mean "eq"?', $error->getMessage()); + $this->assertEquals('Field "neq" is not defined by type "Filters_String_3d2660e0d014aec30b0fc5d8fef65535". Did you mean "eq"?', $error->getMessage()); } $query = '{ artists { edges { node { performances ( filter: {venue: { contains: "test" } } ) { edges { node { venue } } } } } } }'; $result = GraphQL::executeQuery($schema, $query); foreach ($result->errors as $error) { - $this->assertEquals('Field "contains" is not defined by type "Filters_String_e55a7b533af3c46236f06d0fb99f08c6". Did you mean "notin"?', $error->getMessage()); + $this->assertEquals('Field "contains" is not defined by type "Filters_String_3d2660e0d014aec30b0fc5d8fef65535". Did you mean "notin"?', $error->getMessage()); } } } diff --git a/test/Feature/Resolve/EntityFilterTest.php b/test/Feature/Resolve/EntityFilterTest.php index 34bcb4f..b37717e 100644 --- a/test/Feature/Resolve/EntityFilterTest.php +++ b/test/Feature/Resolve/EntityFilterTest.php @@ -256,7 +256,7 @@ public function testnotin(Schema $schema): void /** @dataProvider schemaProvider */ public function testsort(Schema $schema): void { - $query = '{ performance ( filter: {artist: { eq: 1 } id: { sort: "desc" } } ) { edges { node { id } } } }'; + $query = '{ performance ( filter: {artist: { eq: 1 } id: { sort: "desc" sortPriority: 1 } } ) { edges { node { id } } } }'; $result = GraphQL::executeQuery($schema, $query); $data = $result->toArray()['data']; @@ -264,7 +264,7 @@ public function testsort(Schema $schema): void $this->assertEquals(5, count($data['performance']['edges'])); $this->assertEquals(5, $data['performance']['edges'][0]['node']['id']); - $query = '{ performance ( filter: {artist: { eq: 1 } venue: { sort: "asc" } } ) { edges { node { id } } } }'; + $query = '{ performance ( filter: {artist: { eq: 1 } venue: { sort: "asc" sortPriority: 1 } } ) { edges { node { id } } } }'; $result = GraphQL::executeQuery($schema, $query); $data = $result->toArray()['data']; @@ -272,7 +272,7 @@ public function testsort(Schema $schema): void $this->assertEquals(5, count($data['performance']['edges'])); $this->assertEquals(5, $data['performance']['edges'][0]['node']['id']); - $query = '{ performance ( filter: {artist: { eq: 1 } venue: { sort: "desc" } } ) { edges { node { id } } } }'; + $query = '{ performance ( filter: {artist: { eq: 1 } venue: { sort: "desc" sortPriority: 1 } } ) { edges { node { id } } } }'; $result = GraphQL::executeQuery($schema, $query); $data = $result->toArray()['data']; From 92276dd45f62469c2784c139b67e7e7c48749650 Mon Sep 17 00:00:00 2001 From: Tom H Anderson Date: Tue, 19 Aug 2025 15:32:25 -0600 Subject: [PATCH 06/18] Tests working with addition of new Phish performance --- test/AbstractTest.php | 7 +++++++ test/Feature/Event/EntityEventNameTest.php | 5 +++++ test/Feature/Event/EntityFilterTest.php | 3 +++ test/Feature/Metadata/LimitTest.php | 4 ++-- test/Feature/Resolve/EntityFilterTest.php | 12 ++++++++++++ test/Feature/Type/PaginationTest.php | 4 ++-- 6 files changed, 31 insertions(+), 4 deletions(-) diff --git a/test/AbstractTest.php b/test/AbstractTest.php index 5a7f9bb..2d67ed9 100644 --- a/test/AbstractTest.php +++ b/test/AbstractTest.php @@ -109,6 +109,13 @@ protected function populateData(): void 'city' => 'Big Cypress', 'state' => 'Florida', ], + // A second performance at the same venue is needed to test + // sortPriority + '1997-11-14T00:00:00+00:00' => [ + 'venue' => 'E Center', + 'city' => 'West Valley City', + 'state' => 'Utah', + ], ], 'String Cheese Incident' => [ '2002-06-21T00:00:00+00:00' => [ diff --git a/test/Feature/Event/EntityEventNameTest.php b/test/Feature/Event/EntityEventNameTest.php index 0eebe1b..a3c37d5 100644 --- a/test/Feature/Event/EntityEventNameTest.php +++ b/test/Feature/Event/EntityEventNameTest.php @@ -97,6 +97,11 @@ static function (QueryBuilder $event): void { moreFilters: { performanceCount_gte: 3 } + filter: { + name: { + eq: "Grateful Dead" + } + } ) { edges { node { diff --git a/test/Feature/Event/EntityFilterTest.php b/test/Feature/Event/EntityFilterTest.php index ca29cb8..f2006db 100644 --- a/test/Feature/Event/EntityFilterTest.php +++ b/test/Feature/Event/EntityFilterTest.php @@ -95,6 +95,9 @@ static function (QueryBuilder $event): void { moreFilters: { performanceCount_gte: 3 } + filter: { + name: { eq: "Grateful Dead" } + } ) { edges { node { diff --git a/test/Feature/Metadata/LimitTest.php b/test/Feature/Metadata/LimitTest.php index 75e423f..c9687e9 100644 --- a/test/Feature/Metadata/LimitTest.php +++ b/test/Feature/Metadata/LimitTest.php @@ -54,7 +54,7 @@ public function testEntityLimit(): void $data = $result->toArray()['data']; - $this->assertEquals(9, count($data['performance']['edges'])); + $this->assertEquals(10, count($data['performance']['edges'])); $query = '{ artist { edges { node { id performances { edges { node { id } } } } } } }'; $result = GraphQL::executeQuery($schema, $query); @@ -62,6 +62,6 @@ public function testEntityLimit(): void $data = $result->toArray()['data']; $this->assertEquals(2, count($data['artist']['edges'])); $this->assertEquals(5, count($data['artist']['edges'][0]['node']['performances']['edges'])); - $this->assertEquals(2, count($data['artist']['edges'][1]['node']['performances']['edges'])); + $this->assertEquals(3, count($data['artist']['edges'][1]['node']['performances']['edges'])); } } diff --git a/test/Feature/Resolve/EntityFilterTest.php b/test/Feature/Resolve/EntityFilterTest.php index b37717e..66f979d 100644 --- a/test/Feature/Resolve/EntityFilterTest.php +++ b/test/Feature/Resolve/EntityFilterTest.php @@ -280,4 +280,16 @@ public function testsort(Schema $schema): void $this->assertEquals(5, count($data['performance']['edges'])); $this->assertEquals(4, $data['performance']['edges'][0]['node']['id']); } + + /** @dataProvider schemaProvider */ + public function testsortpriority(Schema $schema): void + { + $query = '{ performance ( filter: { id: { sort: "desc" sortPriority: 1 } } ) { edges { node { id } } } }'; + $result = GraphQL::executeQuery($schema, $query); + + $data = $result->toArray()['data']; + + $this->assertEquals(10, count($data['performance']['edges'])); + $this->assertEquals(10, $data['performance']['edges'][0]['node']['id']); + } } diff --git a/test/Feature/Type/PaginationTest.php b/test/Feature/Type/PaginationTest.php index 50b46d4..57fe984 100644 --- a/test/Feature/Type/PaginationTest.php +++ b/test/Feature/Type/PaginationTest.php @@ -222,7 +222,7 @@ public function testLast(): void $data = $result->toArray()['data']; $this->assertEquals(2, count($data['performance']['edges'])); - $this->assertEquals(8, $data['performance']['edges'][0]['node']['id']); + $this->assertEquals(9, $data['performance']['edges'][0]['node']['id']); } public function testBefore(): void @@ -278,7 +278,7 @@ public function testNegativeOffset(): void $data = $result->toArray()['data']; - $this->assertEquals(9, count($data['performance']['edges'])); + $this->assertEquals(10, count($data['performance']['edges'])); $this->assertEquals(1, $data['performance']['edges'][0]['node']['id']); } } From ab2724ce3e0746db2579c5c91d59ea35f5a85741 Mon Sep 17 00:00:00 2001 From: Tom H Anderson Date: Sun, 31 Aug 2025 19:00:09 -0600 Subject: [PATCH 07/18] Unit tests with additional performance --- test/Feature/Event/EntityFilterTest.php | 5 +- test/Feature/Resolve/EntityFilterTest.php | 101 +++++++++++++++++++++- 2 files changed, 98 insertions(+), 8 deletions(-) diff --git a/test/Feature/Event/EntityFilterTest.php b/test/Feature/Event/EntityFilterTest.php index f2006db..159a75b 100644 --- a/test/Feature/Event/EntityFilterTest.php +++ b/test/Feature/Event/EntityFilterTest.php @@ -93,10 +93,7 @@ static function (QueryBuilder $event): void { { artists ( moreFilters: { - performanceCount_gte: 3 - } - filter: { - name: { eq: "Grateful Dead" } + performanceCount_gte: 4 } ) { edges { diff --git a/test/Feature/Resolve/EntityFilterTest.php b/test/Feature/Resolve/EntityFilterTest.php index 66f979d..521b360 100644 --- a/test/Feature/Resolve/EntityFilterTest.php +++ b/test/Feature/Resolve/EntityFilterTest.php @@ -282,14 +282,107 @@ public function testsort(Schema $schema): void } /** @dataProvider schemaProvider */ - public function testsortpriority(Schema $schema): void + public function testSortPriority(Schema $schema): void { - $query = '{ performance ( filter: { id: { sort: "desc" sortPriority: 1 } } ) { edges { node { id } } } }'; + $query = ' + { + performance ( + filter: { + artist: { + eq: 2 + } + venue: { + eq: "E Center" + sort: "asc" + sortPriority: 1 + } + performanceDate: { + sort: "asc" + sortPriority: 2 + } + } + ) { + edges { + node { + id + } + } + } + } + '; + + $result = GraphQL::executeQuery($schema, $query); + + $data = $result->toArray()['data']; + + $this->assertEquals(8, $data['performance']['edges'][0]['node']['id']); + + $query = ' + { + performance ( + filter: { + artist: { + eq: 2 + } + venue: { + eq: "E Center" + sort: "asc" + sortPriority: 1 + } + performanceDate: { + sort: "desc" + sortPriority: 2 + } + } + ) { + edges { + node { + id + } + } + } + } + '; + $result = GraphQL::executeQuery($schema, $query); $data = $result->toArray()['data']; - $this->assertEquals(10, count($data['performance']['edges'])); - $this->assertEquals(10, $data['performance']['edges'][0]['node']['id']); + $this->assertEquals(6, $data['performance']['edges'][0]['node']['id']); + } + + /** @dataProvider schemaProvider */ + public function testSortPriorityNoSort(Schema $schema): void + { + $query = ' + { + performance ( + filter: { + artist: { + eq: 2 + } + venue: { + eq: "E Center" + sortPriority: 1 + } + performanceDate: { + sortPriority: 2 + } + } + ) { + edges { + node { + id + } + } + } + } + '; + + $result = GraphQL::executeQuery($schema, $query); + + $data = $result->toArray()['errors']; + + $this->assertEquals("Sort direction for field 'entity.venue' is not set but a sortPriority was. Please use the 'sort' filter to set the direction.", $data[0]['message']); } } From 0e5074837328cd2a56cde38ffdcccba2138534d9 Mon Sep 17 00:00:00 2001 From: Tom H Anderson Date: Sun, 31 Aug 2025 19:14:55 -0600 Subject: [PATCH 08/18] Modified Filter QueryBuilder to pass phpstan --- src/Filter/QueryBuilder.php | 87 +++++++++++++++---------------------- 1 file changed, 35 insertions(+), 52 deletions(-) diff --git a/src/Filter/QueryBuilder.php b/src/Filter/QueryBuilder.php index ebd2add..5b6d8f5 100644 --- a/src/Filter/QueryBuilder.php +++ b/src/Filter/QueryBuilder.php @@ -9,9 +9,8 @@ use GraphQL\Error\Error; use function array_flip; -use function implode; +use function in_array; use function key; -use function print_r; use function strcmp; use function strtoupper; use function uasort; @@ -44,23 +43,39 @@ public function apply( foreach ($filters as $filter => $value) { $filter = Filters::from($filter); - switch ($filter) { - case Filters::EQ: - case Filters::NEQ: - case Filters::GT: - case Filters::GTE: - case Filters::LT: - case Filters::LTE: - case Filters::IN: - case Filters::NOTIN: - case Filters::ISNULL: - // These filters are handled by the default method - $this->default($filter, $queryBuilderField, $value, $queryBuilder); - break; - default: - $this->{$filter->value}($queryBuilderField, $value, $queryBuilder); - break; + if ( + in_array($filter, [ + Filters::EQ, + Filters::NEQ, + Filters::GT, + Filters::GTE, + Filters::LT, + Filters::LTE, + Filters::IN, + Filters::NOTIN, + ]) + ) { + $this->default($filter->value, $queryBuilderField, $value, $queryBuilder); + continue; } + + if ($filter === Filters::ISNULL) { + if ($value) { + $queryBuilder + ->andWhere( + $queryBuilder->expr()->isNull($queryBuilderField), + ); + } else { + $queryBuilder + ->andWhere( + $queryBuilder->expr()->isNotNull($queryBuilderField), + ); + } + + continue; + } + + $this->{$filter->value}($queryBuilderField, $value, $queryBuilder); } } @@ -70,44 +85,12 @@ public function apply( /** * For filters that do not have a special method, use this method */ - protected function default(Filters $filter, string $field, mixed $value, DoctrineQueryBuilder $queryBuilder): void + protected function default(string $filterValue, string $field, mixed $value, DoctrineQueryBuilder $queryBuilder): void { - /** - * psalm errors without proper filtering here - */ - switch ($filter) { - case Filters::EQ: - case Filters::NEQ: - case Filters::GT: - case Filters::GTE: - case Filters::LT: - case Filters::LTE: - case Filters::IN: - case Filters::NOTIN: - break; - case Filters::ISNULL: - if ($value) { - $queryBuilder - ->andWhere( - $queryBuilder->expr()->isNull($field), - ); - } else { - $queryBuilder - ->andWhere( - $queryBuilder->expr()->isNotNull($field), - ); - } - - return; - - default: - return; - } - $parameter = 'p' . uniqid(); $queryBuilder ->andWhere( - $queryBuilder->expr()->{$filter->value}($field, ':' . $parameter), + $queryBuilder->expr()->$filterValue($field, ':' . $parameter), ) ->setParameter($parameter, $value); } From f138be26295c1a659518113245f086574a8aff8a Mon Sep 17 00:00:00 2001 From: Tom H Anderson Date: Tue, 2 Sep 2025 19:01:47 -0600 Subject: [PATCH 09/18] All unit tests and coverage passing --- src/Filter/QueryBuilder.php | 13 +---- test/AbstractTest.php | 2 +- test/Feature/Resolve/EntityFilterTest.php | 69 +++++++++++++++++++++++ 3 files changed, 71 insertions(+), 13 deletions(-) diff --git a/src/Filter/QueryBuilder.php b/src/Filter/QueryBuilder.php index 5b6d8f5..7eab235 100644 --- a/src/Filter/QueryBuilder.php +++ b/src/Filter/QueryBuilder.php @@ -60,18 +60,7 @@ public function apply( } if ($filter === Filters::ISNULL) { - if ($value) { - $queryBuilder - ->andWhere( - $queryBuilder->expr()->isNull($queryBuilderField), - ); - } else { - $queryBuilder - ->andWhere( - $queryBuilder->expr()->isNotNull($queryBuilderField), - ); - } - + $this->isnull($queryBuilderField, $value, $queryBuilder); continue; } diff --git a/test/AbstractTest.php b/test/AbstractTest.php index 2d67ed9..38ddd36 100644 --- a/test/AbstractTest.php +++ b/test/AbstractTest.php @@ -105,7 +105,7 @@ protected function populateData(): void 'recordings' => ['AKG480 > Aerco preamp > SBM-1'], ], '1999-12-31T00:00:00+00:00' => [ - 'venue' => null, + 'venue' => 'Big Cypress Seminole Indian Reservation', 'city' => 'Big Cypress', 'state' => 'Florida', ], diff --git a/test/Feature/Resolve/EntityFilterTest.php b/test/Feature/Resolve/EntityFilterTest.php index 521b360..f93c1e4 100644 --- a/test/Feature/Resolve/EntityFilterTest.php +++ b/test/Feature/Resolve/EntityFilterTest.php @@ -351,6 +351,75 @@ public function testSortPriority(Schema $schema): void $this->assertEquals(6, $data['performance']['edges'][0]['node']['id']); } + /** @dataProvider schemaProvider */ + public function testSortPriorityNoPriorityOneField(Schema $schema): void + { + /** + * This tests deprecation of not setting sortPriority when sort is set + */ + $query = ' + { + performance ( + filter: { + artist: { + eq: 2 + } + venue: { + sort: "asc" + } + } + ) { + edges { + node { + id + } + } + } + } + '; + + $result = GraphQL::executeQuery($schema, $query); + + $data = $result->toArray()['data']; + + $this->assertEquals(7, $data['performance']['edges'][0]['node']['id']); + } + + /** @dataProvider schemaProvider */ + public function testSortPriorityNoPriorityTwoFields(Schema $schema): void + { + $query = ' + { + performance ( + filter: { + artist: { + eq: 2 + } + venue: { + eq: "E Center" + sort: "asc" + } + performanceDate: { + sort: "asc" + } + } + ) { + edges { + node { + id + } + } + } + } + '; + + $result = GraphQL::executeQuery($schema, $query); + + $data = $result->toArray()['data']; + + $this->assertEquals(8, $data['performance']['edges'][0]['node']['id']); + } + /** @dataProvider schemaProvider */ public function testSortPriorityNoSort(Schema $schema): void { From 14b48ce5d9fdc9e764ef41c0fcb95c1d3278162b Mon Sep 17 00:00:00 2001 From: Tom H Anderson Date: Tue, 2 Sep 2025 19:03:50 -0600 Subject: [PATCH 10/18] Updated actions for php 8.3 --- .github/workflows/coding-standards.yml | 2 +- .github/workflows/continuous-integration.yml | 3 +-- .github/workflows/static-analysis.yml | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index d6322e5..6c88390 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -15,6 +15,6 @@ jobs: name: "Coding Standards" uses: "doctrine/.github/.github/workflows/coding-standards.yml@1.3.0" with: - php-version: '8.1' + php-version: '8.3' composer-options: '--prefer-dist --ignore-platform-req=php' diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 414e4c0..9b1b470 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -19,9 +19,8 @@ jobs: fail-fast: false matrix: php-version: - - "8.1" - - "8.2" - "8.3" + - "8.4" dependencies: - "highest" - "lowest" diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index aadc3a3..c6ef05b 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: shivammathur/setup-php@v2 with: - php-version: '8.1' + php-version: '8.3' - uses: actions/checkout@v2 From 8fe628d5dd4e10aa3c13eb992fbe04779ca00457 Mon Sep 17 00:00:00 2001 From: Tom H Anderson Date: Wed, 3 Sep 2025 00:42:45 -0600 Subject: [PATCH 11/18] Added sortPriority to docs --- README.md | 1 + docs/queries.rst | 31 ++++++++++++++++--------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 6d61232..fecebb9 100644 --- a/README.md +++ b/README.md @@ -271,6 +271,7 @@ Each field has their own set of filters. Based on the field type, some or all o * startwith - A like query with a wildcard on the right side of the value. * endswith - A like query with a wildcard on the left side of the value. * contains - A like query. +* sort & sortPriority - Sort the results by a field. Use sortPriority to sort by multiple fields. You may [exclude any filter](https://doctrine-orm-graphql.apiskeletons.dev/en/latest/attributes.html#entity) from any entity, association, or globally. diff --git a/docs/queries.rst b/docs/queries.rst index be3cdcf..70c72df 100644 --- a/docs/queries.rst +++ b/docs/queries.rst @@ -56,20 +56,21 @@ specific to the entity they filter upon. Provided Filters:: - eq - Equals; same as name: value. DateTime not supported. See Between. - neq - Not Equals - gt - Greater Than - lt - Less Than - gte - Greater Than or Equal To - lte - Less Than or Equal To - in - Filter for values in an array - notin - Filter for values not in an array - between - Filter between `from` and `to` values. Good substitute for DateTime Equals. - contains - Strings only. Similar to a Like query as `like '%value%'` - startswith - Strings only. A like query from the beginning of the value `like 'value%'` - endswith - Strings only. A like query from the end of the value `like '%value'` - isnull - If `true` return results where the field is null. - sort - Sort the result by this field. Value is 'asc' or 'desc' + eq - Equals; same as name: value. DateTime not supported. See Between. + neq - Not Equals + gt - Greater Than + lt - Less Than + gte - Greater Than or Equal To + lte - Less Than or Equal To + in - Filter for values in an array + notin - Filter for values not in an array + between - Filter between `from` and `to` values. Good substitute for DateTime Equals. + contains - Strings only. Similar to a Like query as `like '%value%'` + startswith - Strings only. A like query from the beginning of the value `like 'value%'` + endswith - Strings only. A like query from the end of the value `like '%value'` + isnull - If `true` return results where the field is null. + sort - Sort the result by this field. Value is 'asc' or 'desc' + sortPriority - Sort priority when multiple sort fields are used. Value is an integer starting at 1. The format for using these filters is: @@ -153,7 +154,7 @@ A complete query for all pagination data: } Cursors are included with each edge. A cursor is a base64 encoded -offset from the beginning of the result set. ``base64_encode('0');`` is +offset from the beginning of the result set. ``base64_encode('0');`` is ``MA==`` to use when creating a paginated query. From 0785ba2e790db4767bf16211a9e793db8fb5a2d4 Mon Sep 17 00:00:00 2001 From: Tom H Anderson Date: Wed, 17 Sep 2025 14:38:35 -0600 Subject: [PATCH 12/18] Trigger actions From 408d3e88adca8dfa3f23d1ec69da9652cb717daa Mon Sep 17 00:00:00 2001 From: Tom H Anderson Date: Mon, 22 Sep 2025 10:08:46 -0600 Subject: [PATCH 13/18] Trigger actions From 9de6813a39003bc9ee53193c1ac27b69f5c1d43c Mon Sep 17 00:00:00 2001 From: Tom H Anderson Date: Sat, 25 Oct 2025 11:16:48 -0600 Subject: [PATCH 14/18] Trigger actions without scruitinizer From 42c5ec1bd447a925b3df17a8aeafd3a4a74aae5c Mon Sep 17 00:00:00 2001 From: Tom H Anderson Date: Sat, 25 Oct 2025 12:46:13 -0600 Subject: [PATCH 15/18] Use ubuntu-latest for actions --- .github/workflows/continuous-integration.yml | 6 +++--- .github/workflows/static-analysis.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 9b1b470..429220f 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -13,7 +13,7 @@ on: jobs: phpunit: name: "PHPUnit" - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-latest" strategy: fail-fast: false @@ -47,7 +47,7 @@ jobs: with: dependency-versions: "${{ matrix.dependencies }}" composer-options: "--prefer-dist" - + - name: "Show Composer packages" run: "composer show" @@ -62,7 +62,7 @@ jobs: upload_coverage: name: "Upload coverage to Codecov" - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-latest" needs: - "phpunit" diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index c6ef05b..9c35ab8 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -13,7 +13,7 @@ on: jobs: psalm: name: "Static Analysis" - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-latest" steps: - uses: shivammathur/setup-php@v2 From 54dc0d6205971081db959d6085a2e43947f4d570 Mon Sep 17 00:00:00 2001 From: Tom H Anderson Date: Sat, 25 Oct 2025 13:01:24 -0600 Subject: [PATCH 16/18] Update coding standards workflow --- .github/workflows/coding-standards.yml | 56 +++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index 6c88390..3c945fd 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -9,12 +9,58 @@ on: branches: - "*.x" - "main" + workflow_call: + inputs: + php-version: + description: "The PHP version to use when running the job" + default: "8.3" + required: false + type: "string" + composer-root-version: + description: "The version of the package being tested, in case of circular dependencies." + required: false + type: "string" + composer-dependency-versions: + description: "whether the job should install the locked, highest, or lowest versions of Composer dependencies." + default: "highest" + required: false + type: "string" + composer-options: + description: "Additional flags for the composer install command." + default: "--prefer-dist" + required: false + type: "string" jobs: coding-standards: - name: "Coding Standards" - uses: "doctrine/.github/.github/workflows/coding-standards.yml@1.3.0" - with: - php-version: '8.3' - composer-options: '--prefer-dist --ignore-platform-req=php' + name: "Coding Standards (PHP: ${{ inputs.php-version }})" + runs-on: "ubuntu-22.04" + + steps: + - name: "Checkout" + uses: "actions/checkout@v5" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + ini-file: development + php-version: "${{ inputs.php-version }}" + tools: "cs2pr" + + - name: "Set COMPOSER_ROOT_VERSION" + run: | + echo "COMPOSER_ROOT_VERSION=${{ inputs.composer-root-version }}" >> $GITHUB_ENV + if: "${{ inputs.composer-root-version }}" + + - name: "Install dependencies with Composer" + uses: "ramsey/composer-install@v3" + with: + dependency-versions: "${{ inputs.composer-dependency-versions }}" + composer-options: "${{ inputs.composer-options }}" + + - name: "Run PHP_CodeSniffer" + run: | + vendor/bin/phpcs --report-emacs --report-diff || true + vendor/bin/phpcs --no-colors --report=checkstyle | cs2pr From 6a31906a239885f77bdd05c342ca6a9775289ec8 Mon Sep 17 00:00:00 2001 From: Tom H Anderson Date: Sat, 25 Oct 2025 13:09:34 -0600 Subject: [PATCH 17/18] Update coding standards workflow --- .github/workflows/coding-standards.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index 3c945fd..d02e122 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -62,5 +62,4 @@ jobs: - name: "Run PHP_CodeSniffer" run: | vendor/bin/phpcs --report-emacs --report-diff || true - vendor/bin/phpcs --no-colors --report=checkstyle | cs2pr From e1c37579170d09763ad52b5648be0af9465e9704 Mon Sep 17 00:00:00 2001 From: Tom H Anderson Date: Sat, 25 Oct 2025 13:15:21 -0600 Subject: [PATCH 18/18] Update to Doctrine coding-standard 14.0.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e86e10b..84a57d2 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "league/event": "^3.0" }, "require-dev": { - "doctrine/coding-standard": "^13.0", + "doctrine/coding-standard": "^14.0", "doctrine/dbal": "^3.1 || ^4.0", "phpunit/phpunit": "^9.6", "vimeo/psalm": "^6.13",