From 44c19f909256cbf9e8de772e4e90e58f3754681e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 3 Mar 2026 21:15:32 +1300 Subject: [PATCH 1/4] Extract query lib --- composer.json | 9 +- composer.lock | 124 +++- src/Database/Query.php | 1212 +++------------------------------------- 3 files changed, 177 insertions(+), 1168 deletions(-) diff --git a/composer.json b/composer.json index 5a3a18f3b..7ce20b2ff 100755 --- a/composer.json +++ b/composer.json @@ -40,7 +40,8 @@ "utopia-php/framework": "0.33.*", "utopia-php/cache": "1.*", "utopia-php/pools": "1.*", - "utopia-php/mongo": "1.*" + "utopia-php/mongo": "1.*", + "utopia-php/query": "0.1.*" }, "require-dev": { "fakerphp/faker": "1.23.*", @@ -58,6 +59,12 @@ "mongodb/mongodb": "Needed to support MongoDB Database Adapter" }, + "repositories": [ + { + "type": "vcs", + "url": "git@github.com:utopia-php/query.git" + } + ], "config": { "allow-plugins": { "php-http/discovery": false, diff --git a/composer.lock b/composer.lock index f39de53f8..ea820e2d8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f54c8e057ae09c701c2ce792e00543e8", + "content-hash": "a2b14ee33907216af37002e55a7ff2fe", "packages": [ { "name": "brick/math", @@ -1383,16 +1383,16 @@ }, { "name": "symfony/http-client", - "version": "v7.4.5", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f" + "reference": "2bde8afd5ab2fe0b05a9c2d4c3c0e28ceb98a154" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f", - "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f", + "url": "https://api.github.com/repos/symfony/http-client/zipball/2bde8afd5ab2fe0b05a9c2d4c3c0e28ceb98a154", + "reference": "2bde8afd5ab2fe0b05a9c2d4c3c0e28ceb98a154", "shasum": "" }, "require": { @@ -1460,7 +1460,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.4.5" + "source": "https://github.com/symfony/http-client/tree/v7.4.6" }, "funding": [ { @@ -1480,7 +1480,7 @@ "type": "tidelift" } ], - "time": "2026-01-27T16:16:02+00:00" + "time": "2026-02-18T09:46:18+00:00" }, { "name": "symfony/http-client-contracts", @@ -2078,20 +2078,20 @@ }, { "name": "utopia-php/compression", - "version": "0.1.3", + "version": "0.1.4", "source": { "type": "git", "url": "https://github.com/utopia-php/compression.git", - "reference": "66f093557ba66d98245e562036182016c7dcfe8a" + "reference": "68045cb9d714c1259582d2dfd0e76bd34f83e713" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/compression/zipball/66f093557ba66d98245e562036182016c7dcfe8a", - "reference": "66f093557ba66d98245e562036182016c7dcfe8a", + "url": "https://api.github.com/repos/utopia-php/compression/zipball/68045cb9d714c1259582d2dfd0e76bd34f83e713", + "reference": "68045cb9d714c1259582d2dfd0e76bd34f83e713", "shasum": "" }, "require": { - "php": ">=8.0" + "php": ">=8.1" }, "require-dev": { "laravel/pint": "1.2.*", @@ -2118,22 +2118,22 @@ ], "support": { "issues": "https://github.com/utopia-php/compression/issues", - "source": "https://github.com/utopia-php/compression/tree/0.1.3" + "source": "https://github.com/utopia-php/compression/tree/0.1.4" }, - "time": "2025-01-15T15:15:51+00:00" + "time": "2026-02-17T05:53:40+00:00" }, { "name": "utopia-php/framework", - "version": "0.33.39", + "version": "0.33.41", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "409a258814d664d3a50fa2f48b6695679334d30b" + "reference": "0f3bf2377c867e547c929c3733b8224afee6ef06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/409a258814d664d3a50fa2f48b6695679334d30b", - "reference": "409a258814d664d3a50fa2f48b6695679334d30b", + "url": "https://api.github.com/repos/utopia-php/http/zipball/0f3bf2377c867e547c929c3733b8224afee6ef06", + "reference": "0f3bf2377c867e547c929c3733b8224afee6ef06", "shasum": "" }, "require": { @@ -2167,9 +2167,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.39" + "source": "https://github.com/utopia-php/http/tree/0.33.41" }, - "time": "2026-02-11T06:33:42+00:00" + "time": "2026-02-24T12:01:28+00:00" }, { "name": "utopia-php/mongo", @@ -2234,16 +2234,16 @@ }, { "name": "utopia-php/pools", - "version": "1.0.2", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/utopia-php/pools.git", - "reference": "b7d8dd00306cdd8bf3ff6f1dc90caeaf27dabeb1" + "reference": "74de7c5457a2c447f27e7ec4d72e8412a7d68c10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/pools/zipball/b7d8dd00306cdd8bf3ff6f1dc90caeaf27dabeb1", - "reference": "b7d8dd00306cdd8bf3ff6f1dc90caeaf27dabeb1", + "url": "https://api.github.com/repos/utopia-php/pools/zipball/74de7c5457a2c447f27e7ec4d72e8412a7d68c10", + "reference": "74de7c5457a2c447f27e7ec4d72e8412a7d68c10", "shasum": "" }, "require": { @@ -2281,9 +2281,73 @@ ], "support": { "issues": "https://github.com/utopia-php/pools/issues", - "source": "https://github.com/utopia-php/pools/tree/1.0.2" + "source": "https://github.com/utopia-php/pools/tree/1.0.3" + }, + "time": "2026-02-26T08:42:40+00:00" + }, + { + "name": "utopia-php/query", + "version": "0.1.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/query.git", + "reference": "601490f2967f7b628d4fb62994ba39fe119907db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/query/zipball/601490f2967f7b628d4fb62994ba39fe119907db", + "reference": "601490f2967f7b628d4fb62994ba39fe119907db", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "laravel/pint": "*", + "phpstan/phpstan": "*", + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Query\\": "src/Query" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\Query\\": "tests/Query" + } + }, + "scripts": { + "test": [ + "vendor/bin/phpunit --configuration phpunit.xml" + ], + "lint": [ + "php -d memory_limit=2G ./vendor/bin/pint --test" + ], + "format": [ + "php -d memory_limit=2G ./vendor/bin/pint" + ], + "check": [ + "./vendor/bin/phpstan analyse --level max src tests --memory-limit 2G" + ] + }, + "license": [ + "MIT" + ], + "description": "A simple library providing a query abstraction for filtering, ordering, and pagination", + "keywords": [ + "framework", + "php", + "query", + "upf", + "utopia" + ], + "support": { + "source": "https://github.com/utopia-php/query/tree/0.1.0", + "issues": "https://github.com/utopia-php/query/issues" }, - "time": "2026-01-28T13:12:36+00:00" + "time": "2026-03-03T07:49:53+00:00" }, { "name": "utopia-php/telemetry", @@ -2851,11 +2915,11 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.32", + "version": "1.12.33", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8", - "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/37982d6fc7cbb746dda7773530cda557cdf119e1", + "reference": "37982d6fc7cbb746dda7773530cda557cdf119e1", "shasum": "" }, "require": { @@ -2900,7 +2964,7 @@ "type": "github" } ], - "time": "2025-09-30T10:16:31+00:00" + "time": "2026-02-28T20:30:03+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/src/Database/Query.php b/src/Database/Query.php index 686a6ab37..b33660c37 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -2,895 +2,114 @@ namespace Utopia\Database; -use JsonException; use Utopia\Database\Exception\Query as QueryException; +use Utopia\Query\Exception as BaseQueryException; +use Utopia\Query\Query as BaseQuery; -class Query +class Query extends BaseQuery { - // Filter methods - public const TYPE_EQUAL = 'equal'; - public const TYPE_NOT_EQUAL = 'notEqual'; - public const TYPE_LESSER = 'lessThan'; - public const TYPE_LESSER_EQUAL = 'lessThanEqual'; - public const TYPE_GREATER = 'greaterThan'; - public const TYPE_GREATER_EQUAL = 'greaterThanEqual'; - public const TYPE_CONTAINS = 'contains'; - public const TYPE_CONTAINS_ANY = 'containsAny'; - public const TYPE_NOT_CONTAINS = 'notContains'; - public const TYPE_SEARCH = 'search'; - public const TYPE_NOT_SEARCH = 'notSearch'; - public const TYPE_IS_NULL = 'isNull'; - public const TYPE_IS_NOT_NULL = 'isNotNull'; - public const TYPE_BETWEEN = 'between'; - public const TYPE_NOT_BETWEEN = 'notBetween'; - public const TYPE_STARTS_WITH = 'startsWith'; - public const TYPE_NOT_STARTS_WITH = 'notStartsWith'; - public const TYPE_ENDS_WITH = 'endsWith'; - public const TYPE_NOT_ENDS_WITH = 'notEndsWith'; - public const TYPE_REGEX = 'regex'; - public const TYPE_EXISTS = 'exists'; - public const TYPE_NOT_EXISTS = 'notExists'; - - // Spatial methods - public const TYPE_CROSSES = 'crosses'; - public const TYPE_NOT_CROSSES = 'notCrosses'; - public const TYPE_DISTANCE_EQUAL = 'distanceEqual'; - public const TYPE_DISTANCE_NOT_EQUAL = 'distanceNotEqual'; - public const TYPE_DISTANCE_GREATER_THAN = 'distanceGreaterThan'; - public const TYPE_DISTANCE_LESS_THAN = 'distanceLessThan'; - public const TYPE_INTERSECTS = 'intersects'; - public const TYPE_NOT_INTERSECTS = 'notIntersects'; - public const TYPE_OVERLAPS = 'overlaps'; - public const TYPE_NOT_OVERLAPS = 'notOverlaps'; - public const TYPE_TOUCHES = 'touches'; - public const TYPE_NOT_TOUCHES = 'notTouches'; - - // Vector query methods - public const TYPE_VECTOR_DOT = 'vectorDot'; - public const TYPE_VECTOR_COSINE = 'vectorCosine'; - public const TYPE_VECTOR_EUCLIDEAN = 'vectorEuclidean'; - - public const TYPE_SELECT = 'select'; - - // Order methods - public const TYPE_ORDER_DESC = 'orderDesc'; - public const TYPE_ORDER_ASC = 'orderAsc'; - public const TYPE_ORDER_RANDOM = 'orderRandom'; - - // Pagination methods - public const TYPE_LIMIT = 'limit'; - public const TYPE_OFFSET = 'offset'; - public const TYPE_CURSOR_AFTER = 'cursorAfter'; - public const TYPE_CURSOR_BEFORE = 'cursorBefore'; - - // Logical methods - public const TYPE_AND = 'and'; - public const TYPE_OR = 'or'; - public const TYPE_CONTAINS_ALL = 'containsAll'; - public const TYPE_ELEM_MATCH = 'elemMatch'; - public const DEFAULT_ALIAS = 'main'; - - public const TYPES = [ - self::TYPE_EQUAL, - self::TYPE_NOT_EQUAL, - self::TYPE_LESSER, - self::TYPE_LESSER_EQUAL, - self::TYPE_GREATER, - self::TYPE_GREATER_EQUAL, - self::TYPE_CONTAINS, - self::TYPE_CONTAINS_ANY, - self::TYPE_NOT_CONTAINS, - self::TYPE_SEARCH, - self::TYPE_NOT_SEARCH, - self::TYPE_IS_NULL, - self::TYPE_IS_NOT_NULL, - self::TYPE_BETWEEN, - self::TYPE_NOT_BETWEEN, - self::TYPE_STARTS_WITH, - self::TYPE_NOT_STARTS_WITH, - self::TYPE_ENDS_WITH, - self::TYPE_NOT_ENDS_WITH, - self::TYPE_CROSSES, - self::TYPE_NOT_CROSSES, - self::TYPE_DISTANCE_EQUAL, - self::TYPE_DISTANCE_NOT_EQUAL, - self::TYPE_DISTANCE_GREATER_THAN, - self::TYPE_DISTANCE_LESS_THAN, - self::TYPE_INTERSECTS, - self::TYPE_NOT_INTERSECTS, - self::TYPE_OVERLAPS, - self::TYPE_NOT_OVERLAPS, - self::TYPE_TOUCHES, - self::TYPE_NOT_TOUCHES, - self::TYPE_VECTOR_DOT, - self::TYPE_VECTOR_COSINE, - self::TYPE_VECTOR_EUCLIDEAN, - self::TYPE_EXISTS, - self::TYPE_NOT_EXISTS, - self::TYPE_SELECT, - self::TYPE_ORDER_DESC, - self::TYPE_ORDER_ASC, - self::TYPE_ORDER_RANDOM, - self::TYPE_LIMIT, - self::TYPE_OFFSET, - self::TYPE_CURSOR_AFTER, - self::TYPE_CURSOR_BEFORE, - self::TYPE_AND, - self::TYPE_OR, - self::TYPE_CONTAINS_ALL, - self::TYPE_ELEM_MATCH, - self::TYPE_REGEX - ]; - - public const VECTOR_TYPES = [ - self::TYPE_VECTOR_DOT, - self::TYPE_VECTOR_COSINE, - self::TYPE_VECTOR_EUCLIDEAN, - ]; - - protected const LOGICAL_TYPES = [ - self::TYPE_AND, - self::TYPE_OR, - self::TYPE_ELEM_MATCH, - ]; - - protected string $method = ''; - protected string $attribute = ''; - protected string $attributeType = ''; - protected bool $onArray = false; - protected bool $isObjectAttribute = false; - - /** - * @var array - */ - protected array $values = []; - - /** - * Construct a new query object - * - * @param string $method - * @param string $attribute - * @param array $values - */ - public function __construct(string $method, string $attribute = '', array $values = []) - { - if ($attribute === '' && \in_array($method, [Query::TYPE_ORDER_ASC, Query::TYPE_ORDER_DESC])) { - $attribute = '$sequence'; - } - - $this->method = $method; - $this->attribute = $attribute; - $this->values = $values; - } - - public function __clone(): void - { - foreach ($this->values as $index => $value) { - if ($value instanceof self) { - $this->values[$index] = clone $value; - } - } - } - - /** - * @return string - */ - public function getMethod(): string - { - return $this->method; - } - - /** - * @return string - */ - public function getAttribute(): string - { - return $this->attribute; - } - - /** - * @return array - */ - public function getValues(): array - { - return $this->values; - } - - /** - * @param mixed $default - * @return mixed - */ - public function getValue(mixed $default = null): mixed - { - return $this->values[0] ?? $default; - } - - /** - * Sets method - * - * @param string $method - * @return self - */ - public function setMethod(string $method): self - { - $this->method = $method; - - return $this; - } - - /** - * Sets attribute - * - * @param string $attribute - * @return self - */ - public function setAttribute(string $attribute): self - { - $this->attribute = $attribute; - - return $this; - } - - /** - * Sets values - * - * @param array $values - * @return self - */ - public function setValues(array $values): self - { - $this->values = $values; - - return $this; - } - - /** - * Sets value - * @param mixed $value - * @return self - */ - public function setValue(mixed $value): self - { - $this->values = [$value]; - - return $this; - } - - /** - * Check if method is supported - * - * @param string $value - * @return bool - */ - public static function isMethod(string $value): bool - { - return match ($value) { - self::TYPE_EQUAL, - self::TYPE_NOT_EQUAL, - self::TYPE_LESSER, - self::TYPE_LESSER_EQUAL, - self::TYPE_GREATER, - self::TYPE_GREATER_EQUAL, - self::TYPE_CONTAINS, - self::TYPE_CONTAINS_ANY, - self::TYPE_NOT_CONTAINS, - self::TYPE_SEARCH, - self::TYPE_NOT_SEARCH, - self::TYPE_ORDER_ASC, - self::TYPE_ORDER_DESC, - self::TYPE_ORDER_RANDOM, - self::TYPE_LIMIT, - self::TYPE_OFFSET, - self::TYPE_CURSOR_AFTER, - self::TYPE_CURSOR_BEFORE, - self::TYPE_IS_NULL, - self::TYPE_IS_NOT_NULL, - self::TYPE_BETWEEN, - self::TYPE_NOT_BETWEEN, - self::TYPE_STARTS_WITH, - self::TYPE_NOT_STARTS_WITH, - self::TYPE_ENDS_WITH, - self::TYPE_NOT_ENDS_WITH, - self::TYPE_CROSSES, - self::TYPE_NOT_CROSSES, - self::TYPE_DISTANCE_EQUAL, - self::TYPE_DISTANCE_NOT_EQUAL, - self::TYPE_DISTANCE_GREATER_THAN, - self::TYPE_DISTANCE_LESS_THAN, - self::TYPE_INTERSECTS, - self::TYPE_NOT_INTERSECTS, - self::TYPE_OVERLAPS, - self::TYPE_NOT_OVERLAPS, - self::TYPE_TOUCHES, - self::TYPE_NOT_TOUCHES, - self::TYPE_OR, - self::TYPE_AND, - self::TYPE_CONTAINS_ALL, - self::TYPE_ELEM_MATCH, - self::TYPE_SELECT, - self::TYPE_VECTOR_DOT, - self::TYPE_VECTOR_COSINE, - self::TYPE_VECTOR_EUCLIDEAN, - self::TYPE_EXISTS, - self::TYPE_NOT_EXISTS => true, - default => false, - }; - } - - /** - * Check if method is a spatial-only query method - * @return bool - */ - public function isSpatialQuery(): bool - { - return match ($this->method) { - self::TYPE_CROSSES, - self::TYPE_NOT_CROSSES, - self::TYPE_DISTANCE_EQUAL, - self::TYPE_DISTANCE_NOT_EQUAL, - self::TYPE_DISTANCE_GREATER_THAN, - self::TYPE_DISTANCE_LESS_THAN, - self::TYPE_INTERSECTS, - self::TYPE_NOT_INTERSECTS, - self::TYPE_OVERLAPS, - self::TYPE_NOT_OVERLAPS, - self::TYPE_TOUCHES, - self::TYPE_NOT_TOUCHES => true, - default => false, - }; - } - - /** - * Parse query - * - * @param string $query - * @return self - * @throws QueryException - */ - public static function parse(string $query): self - { - try { - $query = \json_decode($query, true, flags: JSON_THROW_ON_ERROR); - } catch (\JsonException $e) { - throw new QueryException('Invalid query: ' . $e->getMessage()); - } - - if (!\is_array($query)) { - throw new QueryException('Invalid query. Must be an array, got ' . \gettype($query)); - } - - return self::parseQuery($query); - } - - /** - * Parse query - * - * @param array $query - * @return self - * @throws QueryException - */ - public static function parseQuery(array $query): self - { - $method = $query['method'] ?? ''; - $attribute = $query['attribute'] ?? ''; - $values = $query['values'] ?? []; - - if (!\is_string($method)) { - throw new QueryException('Invalid query method. Must be a string, got ' . \gettype($method)); - } - - if (!self::isMethod($method)) { - throw new QueryException('Invalid query method: ' . $method); - } - - if (!\is_string($attribute)) { - throw new QueryException('Invalid query attribute. Must be a string, got ' . \gettype($attribute)); - } - - if (!\is_array($values)) { - throw new QueryException('Invalid query values. Must be an array, got ' . \gettype($values)); - } - - if (\in_array($method, self::LOGICAL_TYPES)) { - foreach ($values as $index => $value) { - $values[$index] = self::parseQuery($value); - } - } - - return new self($method, $attribute, $values); - } - - /** - * Parse an array of queries - * - * @param array $queries - * - * @return array - * @throws QueryException - */ - public static function parseQueries(array $queries): array - { - $parsed = []; - - foreach ($queries as $query) { - $parsed[] = Query::parse($query); - } - - return $parsed; - } - - /** - * @return array - */ - public function toArray(): array - { - $array = ['method' => $this->method]; - - if (!empty($this->attribute)) { - $array['attribute'] = $this->attribute; - } - - if (\in_array($array['method'], self::LOGICAL_TYPES)) { - foreach ($this->values as $index => $value) { - $array['values'][$index] = $value->toArray(); - } - } else { - $array['values'] = []; - foreach ($this->values as $value) { - if ($value instanceof Document && in_array($this->method, [self::TYPE_CURSOR_AFTER, self::TYPE_CURSOR_BEFORE])) { - $value = $value->getId(); - } - $array['values'][] = $value; - } - } - - return $array; - } - - /** - * @return string - * @throws QueryException - */ - public function toString(): string - { - try { - return \json_encode($this->toArray(), flags: JSON_THROW_ON_ERROR); - } catch (JsonException $e) { - throw new QueryException('Invalid Json: ' . $e->getMessage()); - } - } - - /** - * Helper method to create Query with equal method - * - * @param string $attribute - * @param array> $values - * @return Query - */ - public static function equal(string $attribute, array $values): self - { - return new self(self::TYPE_EQUAL, $attribute, $values); - } - - /** - * Helper method to create Query with notEqual method - * - * @param string $attribute - * @param string|int|float|bool|array $value - * @return Query - */ - public static function notEqual(string $attribute, string|int|float|bool|array $value): self - { - // maps or not an array - if ((is_array($value) && !array_is_list($value)) || !is_array($value)) { - $value = [$value]; - } - return new self(self::TYPE_NOT_EQUAL, $attribute, $value); - } - - /** - * Helper method to create Query with lessThan method - * - * @param string $attribute - * @param string|int|float|bool $value - * @return Query - */ - public static function lessThan(string $attribute, string|int|float|bool $value): self - { - return new self(self::TYPE_LESSER, $attribute, [$value]); - } - - /** - * Helper method to create Query with lessThanEqual method - * - * @param string $attribute - * @param string|int|float|bool $value - * @return Query - */ - public static function lessThanEqual(string $attribute, string|int|float|bool $value): self - { - return new self(self::TYPE_LESSER_EQUAL, $attribute, [$value]); - } - - /** - * Helper method to create Query with greaterThan method - * - * @param string $attribute - * @param string|int|float|bool $value - * @return Query - */ - public static function greaterThan(string $attribute, string|int|float|bool $value): self - { - return new self(self::TYPE_GREATER, $attribute, [$value]); - } - - /** - * Helper method to create Query with greaterThanEqual method - * - * @param string $attribute - * @param string|int|float|bool $value - * @return Query - */ - public static function greaterThanEqual(string $attribute, string|int|float|bool $value): self - { - return new self(self::TYPE_GREATER_EQUAL, $attribute, [$value]); - } - - /** - * Helper method to create Query with contains method - * - * @deprecated Use containsAny() for array attributes, or keep using contains() for string substring matching. - * @param string $attribute - * @param array $values - * @return Query - */ - public static function contains(string $attribute, array $values): self - { - return new self(self::TYPE_CONTAINS, $attribute, $values); - } - - /** - * Helper method to create Query with containsAny method. - * For array and relationship attributes, matches documents where the attribute contains ANY of the given values. - * - * @param string $attribute - * @param array $values - * @return Query - */ - public static function containsAny(string $attribute, array $values): self - { - return new self(self::TYPE_CONTAINS_ANY, $attribute, $values); - } - - /** - * Helper method to create Query with notContains method - * - * @param string $attribute - * @param array $values - * @return Query - */ - public static function notContains(string $attribute, array $values): self - { - return new self(self::TYPE_NOT_CONTAINS, $attribute, $values); - } - - /** - * Helper method to create Query with between method - * - * @param string $attribute - * @param string|int|float|bool $start - * @param string|int|float|bool $end - * @return Query - */ - public static function between(string $attribute, string|int|float|bool $start, string|int|float|bool $end): self - { - return new self(self::TYPE_BETWEEN, $attribute, [$start, $end]); - } - - /** - * Helper method to create Query with notBetween method - * - * @param string $attribute - * @param string|int|float|bool $start - * @param string|int|float|bool $end - * @return Query - */ - public static function notBetween(string $attribute, string|int|float|bool $start, string|int|float|bool $end): self - { - return new self(self::TYPE_NOT_BETWEEN, $attribute, [$start, $end]); - } - - /** - * Helper method to create Query with search method - * - * @param string $attribute - * @param string $value - * @return Query - */ - public static function search(string $attribute, string $value): self - { - return new self(self::TYPE_SEARCH, $attribute, [$value]); - } - - /** - * Helper method to create Query with notSearch method - * - * @param string $attribute - * @param string $value - * @return Query - */ - public static function notSearch(string $attribute, string $value): self - { - return new self(self::TYPE_NOT_SEARCH, $attribute, [$value]); - } - - /** - * Helper method to create Query with select method - * - * @param array $attributes - * @return Query - */ - public static function select(array $attributes): self - { - return new self(self::TYPE_SELECT, values: $attributes); - } - - /** - * Helper method to create Query with orderDesc method - * - * @param string $attribute - * @return Query - */ - public static function orderDesc(string $attribute = ''): self - { - return new self(self::TYPE_ORDER_DESC, $attribute); - } - - /** - * Helper method to create Query with orderAsc method - * - * @param string $attribute - * @return Query - */ - public static function orderAsc(string $attribute = ''): self - { - return new self(self::TYPE_ORDER_ASC, $attribute); - } - - /** - * Helper method to create Query with orderRandom method - * - * @return Query - */ - public static function orderRandom(): self - { - return new self(self::TYPE_ORDER_RANDOM); - } - - /** - * Helper method to create Query with limit method - * - * @param int $value - * @return Query - */ - public static function limit(int $value): self - { - return new self(self::TYPE_LIMIT, values: [$value]); - } - - /** - * Helper method to create Query with offset method - * - * @param int $value - * @return Query - */ - public static function offset(int $value): self - { - return new self(self::TYPE_OFFSET, values: [$value]); - } - - /** - * Helper method to create Query with cursorAfter method - * - * @param Document $value - * @return Query - */ - public static function cursorAfter(Document $value): self - { - return new self(self::TYPE_CURSOR_AFTER, values: [$value]); - } - - /** - * Helper method to create Query with cursorBefore method - * - * @param Document $value - * @return Query - */ - public static function cursorBefore(Document $value): self - { - return new self(self::TYPE_CURSOR_BEFORE, values: [$value]); - } - - /** - * Helper method to create Query with isNull method - * - * @param string $attribute - * @return Query - */ - public static function isNull(string $attribute): self - { - return new self(self::TYPE_IS_NULL, $attribute); - } - - /** - * Helper method to create Query with isNotNull method - * - * @param string $attribute - * @return Query - */ - public static function isNotNull(string $attribute): self - { - return new self(self::TYPE_IS_NOT_NULL, $attribute); - } - - public static function startsWith(string $attribute, string $value): self - { - return new self(self::TYPE_STARTS_WITH, $attribute, [$value]); - } - - public static function notStartsWith(string $attribute, string $value): self - { - return new self(self::TYPE_NOT_STARTS_WITH, $attribute, [$value]); - } - - public static function endsWith(string $attribute, string $value): self - { - return new self(self::TYPE_ENDS_WITH, $attribute, [$value]); - } - - public static function notEndsWith(string $attribute, string $value): self - { - return new self(self::TYPE_NOT_ENDS_WITH, $attribute, [$value]); - } + protected bool $isObjectAttribute = false; /** - * Helper method to create Query for documents created before a specific date - * - * @param string $value - * @return Query + * @param array $values */ - public static function createdBefore(string $value): self + public function __construct(string $method, string $attribute = '', array $values = []) { - return self::lessThan('$createdAt', $value); - } + if ($attribute === '' && \in_array($method, [self::TYPE_ORDER_ASC, self::TYPE_ORDER_DESC])) { + $attribute = '$sequence'; + } - /** - * Helper method to create Query for documents created after a specific date - * - * @param string $value - * @return Query - */ - public static function createdAfter(string $value): self - { - return self::greaterThan('$createdAt', $value); + parent::__construct($method, $attribute, $values); } /** - * Helper method to create Query for documents updated before a specific date - * - * @param string $value - * @return Query + * @param string $query + * @return self + * @throws QueryException */ - public static function updatedBefore(string $value): self + public static function parse(string $query): self { - return self::lessThan('$updatedAt', $value); + try { + return parent::parse($query); + } catch (BaseQueryException $e) { + if ($e instanceof QueryException) { + throw $e; + } + throw new QueryException($e->getMessage(), $e->getCode(), $e); + } } /** - * Helper method to create Query for documents updated after a specific date - * - * @param string $value - * @return Query + * @param array $query + * @return self + * @throws QueryException */ - public static function updatedAfter(string $value): self + public static function parseQuery(array $query): self { - return self::greaterThan('$updatedAt', $value); + try { + return parent::parseQuery($query); + } catch (BaseQueryException $e) { + if ($e instanceof QueryException) { + throw $e; + } + throw new QueryException($e->getMessage(), $e->getCode(), $e); + } } /** - * Helper method to create Query for documents created between two dates + * Helper method to create Query with cursorAfter method * - * @param string $start - * @param string $end + * @param Document $value * @return Query */ - public static function createdBetween(string $start, string $end): self + public static function cursorAfter(mixed $value): self { - return self::between('$createdAt', $start, $end); + return new self(self::TYPE_CURSOR_AFTER, values: [$value]); } /** - * Helper method to create Query for documents updated between two dates + * Helper method to create Query with cursorBefore method * - * @param string $start - * @param string $end - * @return Query - */ - public static function updatedBetween(string $start, string $end): self - { - return self::between('$updatedAt', $start, $end); - } - - /** - * @param array $queries - * @return Query - */ - public static function or(array $queries): self - { - return new self(self::TYPE_OR, '', $queries); - } - - /** - * @param array $queries + * @param Document $value * @return Query */ - public static function and(array $queries): self + public static function cursorBefore(mixed $value): self { - return new self(self::TYPE_AND, '', $queries); + return new self(self::TYPE_CURSOR_BEFORE, values: [$value]); } /** - * @param string $attribute - * @param array $values - * @return Query + * @return array */ - public static function containsAll(string $attribute, array $values): self + public function toArray(): array { - return new self(self::TYPE_CONTAINS_ALL, $attribute, $values); - } + $array = ['method' => $this->method]; - /** - * Filters $queries for $types - * - * @param array $queries - * @param array $types - * @param bool $clone - * @return array - */ - public static function getByType(array $queries, array $types, bool $clone = true): array - { - $filtered = []; + if (!empty($this->attribute)) { + $array['attribute'] = $this->attribute; + } - foreach ($queries as $query) { - if (\in_array($query->getMethod(), $types, true)) { - $filtered[] = $clone ? clone $query : $query; + if (\in_array($array['method'], static::LOGICAL_TYPES)) { + foreach ($this->values as $index => $value) { + $array['values'][$index] = $value->toArray(); + } + } else { + $array['values'] = []; + foreach ($this->values as $value) { + if ($value instanceof Document && in_array($this->method, [self::TYPE_CURSOR_AFTER, self::TYPE_CURSOR_BEFORE])) { + $value = $value->getId(); + } + $array['values'][] = $value; } } - return $filtered; - } - - /** - * @param array $queries - * @param bool $clone - * @return array - */ - public static function getCursorQueries(array $queries, bool $clone = true): array - { - return self::getByType( - $queries, - [ - Query::TYPE_CURSOR_AFTER, - Query::TYPE_CURSOR_BEFORE, - ], - $clone - ); + return $array; } /** - * Iterates through queries are groups them by type + * Iterates through queries and groups them by type * - * @param array $queries + * @param array $queries * @return array{ * filters: array, * selections: array, @@ -914,7 +133,7 @@ public static function groupByType(array $queries): array $cursorDirection = null; foreach ($queries as $query) { - if (!$query instanceof Query) { + if (!$query instanceof BaseQuery) { continue; } @@ -923,21 +142,21 @@ public static function groupByType(array $queries): array $values = $query->getValues(); switch ($method) { - case Query::TYPE_ORDER_ASC: - case Query::TYPE_ORDER_DESC: - case Query::TYPE_ORDER_RANDOM: + case self::TYPE_ORDER_ASC: + case self::TYPE_ORDER_DESC: + case self::TYPE_ORDER_RANDOM: if (!empty($attribute)) { $orderAttributes[] = $attribute; } $orderTypes[] = match ($method) { - Query::TYPE_ORDER_ASC => Database::ORDER_ASC, - Query::TYPE_ORDER_DESC => Database::ORDER_DESC, - Query::TYPE_ORDER_RANDOM => Database::ORDER_RANDOM, + self::TYPE_ORDER_ASC => Database::ORDER_ASC, + self::TYPE_ORDER_DESC => Database::ORDER_DESC, + self::TYPE_ORDER_RANDOM => Database::ORDER_RANDOM, }; break; - case Query::TYPE_LIMIT: + case self::TYPE_LIMIT: // Keep the 1st limit encountered and ignore the rest if ($limit !== null) { break; @@ -945,7 +164,7 @@ public static function groupByType(array $queries): array $limit = $values[0] ?? $limit; break; - case Query::TYPE_OFFSET: + case self::TYPE_OFFSET: // Keep the 1st offset encountered and ignore the rest if ($offset !== null) { break; @@ -953,18 +172,18 @@ public static function groupByType(array $queries): array $offset = $values[0] ?? $limit; break; - case Query::TYPE_CURSOR_AFTER: - case Query::TYPE_CURSOR_BEFORE: + case self::TYPE_CURSOR_AFTER: + case self::TYPE_CURSOR_BEFORE: // Keep the 1st cursor encountered and ignore the rest if ($cursor !== null) { break; } $cursor = $values[0] ?? $limit; - $cursorDirection = $method === Query::TYPE_CURSOR_AFTER ? Database::CURSOR_AFTER : Database::CURSOR_BEFORE; + $cursorDirection = $method === self::TYPE_CURSOR_AFTER ? Database::CURSOR_AFTER : Database::CURSOR_BEFORE; break; - case Query::TYPE_SELECT: + case self::TYPE_SELECT: $selections[] = clone $query; break; @@ -986,53 +205,6 @@ public static function groupByType(array $queries): array ]; } - /** - * Is this query able to contain other queries - * - * @return bool - */ - public function isNested(): bool - { - if (in_array($this->getMethod(), self::LOGICAL_TYPES)) { - return true; - } - - return false; - } - - /** - * @return bool - */ - public function onArray(): bool - { - return $this->onArray; - } - - /** - * @param bool $bool - * @return void - */ - public function setOnArray(bool $bool): void - { - $this->onArray = $bool; - } - - /** - * @param string $type - * @return void - */ - public function setAttributeType(string $type): void - { - $this->attributeType = $type; - } - - /** - * @return string - */ - public function getAttributeType(): string - { - return $this->attributeType; - } /** * @return bool */ @@ -1048,238 +220,4 @@ public function isObjectAttribute(): bool { return $this->attributeType === Database::VAR_OBJECT; } - - // Spatial query methods - - /** - * Helper method to create Query with distanceEqual method - * - * @param string $attribute - * @param array $values - * @param int|float $distance - * @param bool $meters - * @return Query - */ - public static function distanceEqual(string $attribute, array $values, int|float $distance, bool $meters = false): self - { - return new self(self::TYPE_DISTANCE_EQUAL, $attribute, [[$values,$distance,$meters]]); - } - - /** - * Helper method to create Query with distanceNotEqual method - * - * @param string $attribute - * @param array $values - * @param int|float $distance - * @param bool $meters - * @return Query - */ - public static function distanceNotEqual(string $attribute, array $values, int|float $distance, bool $meters = false): self - { - return new self(self::TYPE_DISTANCE_NOT_EQUAL, $attribute, [[$values,$distance,$meters]]); - } - - /** - * Helper method to create Query with distanceGreaterThan method - * - * @param string $attribute - * @param array $values - * @param int|float $distance - * @param bool $meters - * @return Query - */ - public static function distanceGreaterThan(string $attribute, array $values, int|float $distance, bool $meters = false): self - { - return new self(self::TYPE_DISTANCE_GREATER_THAN, $attribute, [[$values,$distance, $meters]]); - } - - /** - * Helper method to create Query with distanceLessThan method - * - * @param string $attribute - * @param array $values - * @param int|float $distance - * @param bool $meters - * @return Query - */ - public static function distanceLessThan(string $attribute, array $values, int|float $distance, bool $meters = false): self - { - return new self(self::TYPE_DISTANCE_LESS_THAN, $attribute, [[$values,$distance,$meters]]); - } - - /** - * Helper method to create Query with intersects method - * - * @param string $attribute - * @param array $values - * @return Query - */ - public static function intersects(string $attribute, array $values): self - { - return new self(self::TYPE_INTERSECTS, $attribute, [$values]); - } - - /** - * Helper method to create Query with notIntersects method - * - * @param string $attribute - * @param array $values - * @return Query - */ - public static function notIntersects(string $attribute, array $values): self - { - return new self(self::TYPE_NOT_INTERSECTS, $attribute, [$values]); - } - - /** - * Helper method to create Query with crosses method - * - * @param string $attribute - * @param array $values - * @return Query - */ - public static function crosses(string $attribute, array $values): self - { - return new self(self::TYPE_CROSSES, $attribute, [$values]); - } - - /** - * Helper method to create Query with notCrosses method - * - * @param string $attribute - * @param array $values - * @return Query - */ - public static function notCrosses(string $attribute, array $values): self - { - return new self(self::TYPE_NOT_CROSSES, $attribute, [$values]); - } - - /** - * Helper method to create Query with overlaps method - * - * @param string $attribute - * @param array $values - * @return Query - */ - public static function overlaps(string $attribute, array $values): self - { - return new self(self::TYPE_OVERLAPS, $attribute, [$values]); - } - - /** - * Helper method to create Query with notOverlaps method - * - * @param string $attribute - * @param array $values - * @return Query - */ - public static function notOverlaps(string $attribute, array $values): self - { - return new self(self::TYPE_NOT_OVERLAPS, $attribute, [$values]); - } - - /** - * Helper method to create Query with touches method - * - * @param string $attribute - * @param array $values - * @return Query - */ - public static function touches(string $attribute, array $values): self - { - return new self(self::TYPE_TOUCHES, $attribute, [$values]); - } - - /** - * Helper method to create Query with notTouches method - * - * @param string $attribute - * @param array $values - * @return Query - */ - public static function notTouches(string $attribute, array $values): self - { - return new self(self::TYPE_NOT_TOUCHES, $attribute, [$values]); - } - - /** - * Helper method to create Query with vectorDot method - * - * @param string $attribute - * @param array $vector - * @return Query - */ - public static function vectorDot(string $attribute, array $vector): self - { - return new self(self::TYPE_VECTOR_DOT, $attribute, [$vector]); - } - - /** - * Helper method to create Query with vectorCosine method - * - * @param string $attribute - * @param array $vector - * @return Query - */ - public static function vectorCosine(string $attribute, array $vector): self - { - return new self(self::TYPE_VECTOR_COSINE, $attribute, [$vector]); - } - - /** - * Helper method to create Query with vectorEuclidean method - * - * @param string $attribute - * @param array $vector - * @return Query - */ - public static function vectorEuclidean(string $attribute, array $vector): self - { - return new self(self::TYPE_VECTOR_EUCLIDEAN, $attribute, [$vector]); - } - - /** - * Helper method to create Query with regex method - * - * @param string $attribute - * @param string $pattern - * @return Query - */ - public static function regex(string $attribute, string $pattern): self - { - return new self(self::TYPE_REGEX, $attribute, [$pattern]); - } - - /** - * Helper method to create Query with exists method - * - * @param array $attributes - * @return Query - */ - public static function exists(array $attributes): self - { - return new self(self::TYPE_EXISTS, '', $attributes); - } - - /** - * Helper method to create Query with notExists method - * - * @param string|int|float|bool|array $attribute - * @return Query - */ - public static function notExists(string|int|float|bool|array $attribute): self - { - return new self(self::TYPE_NOT_EXISTS, '', is_array($attribute) ? $attribute : [$attribute]); - } - - /** - * @param string $attribute - * @param array $queries - * @return Query - */ - public static function elemMatch(string $attribute, array $queries): self - { - return new self(self::TYPE_ELEM_MATCH, $attribute, $queries); - } } From d942f2b45eccc6a28754db4b23eb360193b06bb7 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 3 Mar 2026 21:38:32 +1300 Subject: [PATCH 2/4] fix: resolve PHPStan type errors from query lib extraction Add a PHPStan stub for Utopia\Query\Query that declares `@return static` on all factory methods, so PHPStan correctly resolves return types when called via the Utopia\Database\Query subclass. Also fix groupByType() param type and remove dead instanceof checks in parse/parseQuery. Co-Authored-By: Claude Opus 4.6 --- phpstan.neon | 3 + src/Database/Query.php | 10 +- stubs/Query.stub | 314 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 319 insertions(+), 8 deletions(-) create mode 100644 phpstan.neon create mode 100644 stubs/Query.stub diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 000000000..34ab081b9 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,3 @@ +parameters: + stubFiles: + - stubs/Query.stub diff --git a/src/Database/Query.php b/src/Database/Query.php index b33660c37..f34611e33 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -32,9 +32,6 @@ public static function parse(string $query): self try { return parent::parse($query); } catch (BaseQueryException $e) { - if ($e instanceof QueryException) { - throw $e; - } throw new QueryException($e->getMessage(), $e->getCode(), $e); } } @@ -49,9 +46,6 @@ public static function parseQuery(array $query): self try { return parent::parseQuery($query); } catch (BaseQueryException $e) { - if ($e instanceof QueryException) { - throw $e; - } throw new QueryException($e->getMessage(), $e->getCode(), $e); } } @@ -109,7 +103,7 @@ public function toArray(): array /** * Iterates through queries and groups them by type * - * @param array $queries + * @param array $queries * @return array{ * filters: array, * selections: array, @@ -133,7 +127,7 @@ public static function groupByType(array $queries): array $cursorDirection = null; foreach ($queries as $query) { - if (!$query instanceof BaseQuery) { + if (!$query instanceof self) { continue; } diff --git a/stubs/Query.stub b/stubs/Query.stub new file mode 100644 index 000000000..6decd2890 --- /dev/null +++ b/stubs/Query.stub @@ -0,0 +1,314 @@ + $values */ + public function __construct(string $method, string $attribute = '', array $values = []) {} + + /** @return static */ + public static function parse(string $query): self {} + + /** + * @param array $query + * @return static + */ + public static function parseQuery(array $query): self {} + + /** + * @param array $queries + * @return array + */ + public static function parseQueries(array $queries): array {} + + /** + * @param array> $values + * @return static + */ + public static function equal(string $attribute, array $values): self {} + + /** + * @param string|int|float|bool|array $value + * @return static + */ + public static function notEqual(string $attribute, string|int|float|bool|array $value): self {} + + /** @return static */ + public static function lessThan(string $attribute, string|int|float|bool $value): self {} + + /** @return static */ + public static function lessThanEqual(string $attribute, string|int|float|bool $value): self {} + + /** @return static */ + public static function greaterThan(string $attribute, string|int|float|bool $value): self {} + + /** @return static */ + public static function greaterThanEqual(string $attribute, string|int|float|bool $value): self {} + + /** + * @param array $values + * @return static + */ + public static function contains(string $attribute, array $values): self {} + + /** + * @param array $values + * @return static + */ + public static function containsAny(string $attribute, array $values): self {} + + /** + * @param array $values + * @return static + */ + public static function notContains(string $attribute, array $values): self {} + + /** @return static */ + public static function between(string $attribute, string|int|float|bool $start, string|int|float|bool $end): self {} + + /** @return static */ + public static function notBetween(string $attribute, string|int|float|bool $start, string|int|float|bool $end): self {} + + /** @return static */ + public static function search(string $attribute, string $value): self {} + + /** @return static */ + public static function notSearch(string $attribute, string $value): self {} + + /** + * @param array $attributes + * @return static + */ + public static function select(array $attributes): self {} + + /** @return static */ + public static function orderDesc(string $attribute = ''): self {} + + /** @return static */ + public static function orderAsc(string $attribute = ''): self {} + + /** @return static */ + public static function orderRandom(): self {} + + /** @return static */ + public static function limit(int $value): self {} + + /** @return static */ + public static function offset(int $value): self {} + + /** @return static */ + public static function cursorAfter(mixed $value): self {} + + /** @return static */ + public static function cursorBefore(mixed $value): self {} + + /** @return static */ + public static function isNull(string $attribute): self {} + + /** @return static */ + public static function isNotNull(string $attribute): self {} + + /** @return static */ + public static function startsWith(string $attribute, string $value): self {} + + /** @return static */ + public static function notStartsWith(string $attribute, string $value): self {} + + /** @return static */ + public static function endsWith(string $attribute, string $value): self {} + + /** @return static */ + public static function notEndsWith(string $attribute, string $value): self {} + + /** @return static */ + public static function createdBefore(string $value): self {} + + /** @return static */ + public static function createdAfter(string $value): self {} + + /** @return static */ + public static function updatedBefore(string $value): self {} + + /** @return static */ + public static function updatedAfter(string $value): self {} + + /** @return static */ + public static function createdBetween(string $start, string $end): self {} + + /** @return static */ + public static function updatedBetween(string $start, string $end): self {} + + /** + * @param array $queries + * @return static + */ + public static function or(array $queries): self {} + + /** + * @param array $queries + * @return static + */ + public static function and(array $queries): self {} + + /** + * @param array $values + * @return static + */ + public static function containsAll(string $attribute, array $values): self {} + + /** + * @param array $queries + * @return static + */ + public static function elemMatch(string $attribute, array $queries): self {} + + /** + * @param array $queries + * @param array $types + * @return array + */ + public static function getByType(array $queries, array $types, bool $clone = true): array {} + + /** + * @param array $queries + * @return array + */ + public static function getCursorQueries(array $queries, bool $clone = true): array {} + + /** + * @param array $queries + * @return array{ + * filters: array, + * selections: array, + * limit: int|null, + * offset: int|null, + * orderAttributes: array, + * orderTypes: array, + * cursor: mixed, + * cursorDirection: string|null + * } + */ + public static function groupByType(array $queries): array {} + + /** @return static */ + public function setMethod(string $method): self {} + + /** @return static */ + public function setAttribute(string $attribute): self {} + + /** + * @param array $values + * @return static + */ + public function setValues(array $values): self {} + + /** @return static */ + public function setValue(mixed $value): self {} + + /** + * @param array $values + * @return static + */ + public static function distanceEqual(string $attribute, array $values, int|float $distance, bool $meters = false): self {} + + /** + * @param array $values + * @return static + */ + public static function distanceNotEqual(string $attribute, array $values, int|float $distance, bool $meters = false): self {} + + /** + * @param array $values + * @return static + */ + public static function distanceGreaterThan(string $attribute, array $values, int|float $distance, bool $meters = false): self {} + + /** + * @param array $values + * @return static + */ + public static function distanceLessThan(string $attribute, array $values, int|float $distance, bool $meters = false): self {} + + /** + * @param array $values + * @return static + */ + public static function intersects(string $attribute, array $values): self {} + + /** + * @param array $values + * @return static + */ + public static function notIntersects(string $attribute, array $values): self {} + + /** + * @param array $values + * @return static + */ + public static function crosses(string $attribute, array $values): self {} + + /** + * @param array $values + * @return static + */ + public static function notCrosses(string $attribute, array $values): self {} + + /** + * @param array $values + * @return static + */ + public static function overlaps(string $attribute, array $values): self {} + + /** + * @param array $values + * @return static + */ + public static function notOverlaps(string $attribute, array $values): self {} + + /** + * @param array $values + * @return static + */ + public static function touches(string $attribute, array $values): self {} + + /** + * @param array $values + * @return static + */ + public static function notTouches(string $attribute, array $values): self {} + + /** + * @param array $vector + * @return static + */ + public static function vectorDot(string $attribute, array $vector): self {} + + /** + * @param array $vector + * @return static + */ + public static function vectorCosine(string $attribute, array $vector): self {} + + /** + * @param array $vector + * @return static + */ + public static function vectorEuclidean(string $attribute, array $vector): self {} + + /** @return static */ + public static function regex(string $attribute, string $pattern): self {} + + /** + * @param array $attributes + * @return static + */ + public static function exists(array $attributes): self {} + + /** + * @param string|int|float|bool|array $attribute + * @return static + */ + public static function notExists(string|int|float|bool|array $attribute): self {} +} From b0a1faf6e0c9a07f7ad7cbd09223b1ef78801456 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 3 Mar 2026 22:06:00 +1300 Subject: [PATCH 3/4] fix: use static return types and remove PHPStan stubs Update Query overrides to use `: static` return types matching the base query package. Remove the phpstan.neon and stubs workaround since the query package now uses `: static` natively. Co-Authored-By: Claude Opus 4.6 --- phpstan.neon | 3 - src/Database/Query.php | 22 +-- stubs/Query.stub | 314 ----------------------------------------- 3 files changed, 7 insertions(+), 332 deletions(-) delete mode 100644 phpstan.neon delete mode 100644 stubs/Query.stub diff --git a/phpstan.neon b/phpstan.neon deleted file mode 100644 index 34ab081b9..000000000 --- a/phpstan.neon +++ /dev/null @@ -1,3 +0,0 @@ -parameters: - stubFiles: - - stubs/Query.stub diff --git a/src/Database/Query.php b/src/Database/Query.php index f34611e33..1cd7f8d13 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -6,6 +6,7 @@ use Utopia\Query\Exception as BaseQueryException; use Utopia\Query\Query as BaseQuery; +/** @phpstan-consistent-constructor */ class Query extends BaseQuery { protected bool $isObjectAttribute = false; @@ -23,11 +24,9 @@ public function __construct(string $method, string $attribute = '', array $value } /** - * @param string $query - * @return self * @throws QueryException */ - public static function parse(string $query): self + public static function parse(string $query): static { try { return parent::parse($query); @@ -38,10 +37,9 @@ public static function parse(string $query): self /** * @param array $query - * @return self * @throws QueryException */ - public static function parseQuery(array $query): self + public static function parseQuery(array $query): static { try { return parent::parseQuery($query); @@ -51,25 +49,19 @@ public static function parseQuery(array $query): self } /** - * Helper method to create Query with cursorAfter method - * * @param Document $value - * @return Query */ - public static function cursorAfter(mixed $value): self + public static function cursorAfter(mixed $value): static { - return new self(self::TYPE_CURSOR_AFTER, values: [$value]); + return new static(self::TYPE_CURSOR_AFTER, values: [$value]); } /** - * Helper method to create Query with cursorBefore method - * * @param Document $value - * @return Query */ - public static function cursorBefore(mixed $value): self + public static function cursorBefore(mixed $value): static { - return new self(self::TYPE_CURSOR_BEFORE, values: [$value]); + return new static(self::TYPE_CURSOR_BEFORE, values: [$value]); } /** diff --git a/stubs/Query.stub b/stubs/Query.stub deleted file mode 100644 index 6decd2890..000000000 --- a/stubs/Query.stub +++ /dev/null @@ -1,314 +0,0 @@ - $values */ - public function __construct(string $method, string $attribute = '', array $values = []) {} - - /** @return static */ - public static function parse(string $query): self {} - - /** - * @param array $query - * @return static - */ - public static function parseQuery(array $query): self {} - - /** - * @param array $queries - * @return array - */ - public static function parseQueries(array $queries): array {} - - /** - * @param array> $values - * @return static - */ - public static function equal(string $attribute, array $values): self {} - - /** - * @param string|int|float|bool|array $value - * @return static - */ - public static function notEqual(string $attribute, string|int|float|bool|array $value): self {} - - /** @return static */ - public static function lessThan(string $attribute, string|int|float|bool $value): self {} - - /** @return static */ - public static function lessThanEqual(string $attribute, string|int|float|bool $value): self {} - - /** @return static */ - public static function greaterThan(string $attribute, string|int|float|bool $value): self {} - - /** @return static */ - public static function greaterThanEqual(string $attribute, string|int|float|bool $value): self {} - - /** - * @param array $values - * @return static - */ - public static function contains(string $attribute, array $values): self {} - - /** - * @param array $values - * @return static - */ - public static function containsAny(string $attribute, array $values): self {} - - /** - * @param array $values - * @return static - */ - public static function notContains(string $attribute, array $values): self {} - - /** @return static */ - public static function between(string $attribute, string|int|float|bool $start, string|int|float|bool $end): self {} - - /** @return static */ - public static function notBetween(string $attribute, string|int|float|bool $start, string|int|float|bool $end): self {} - - /** @return static */ - public static function search(string $attribute, string $value): self {} - - /** @return static */ - public static function notSearch(string $attribute, string $value): self {} - - /** - * @param array $attributes - * @return static - */ - public static function select(array $attributes): self {} - - /** @return static */ - public static function orderDesc(string $attribute = ''): self {} - - /** @return static */ - public static function orderAsc(string $attribute = ''): self {} - - /** @return static */ - public static function orderRandom(): self {} - - /** @return static */ - public static function limit(int $value): self {} - - /** @return static */ - public static function offset(int $value): self {} - - /** @return static */ - public static function cursorAfter(mixed $value): self {} - - /** @return static */ - public static function cursorBefore(mixed $value): self {} - - /** @return static */ - public static function isNull(string $attribute): self {} - - /** @return static */ - public static function isNotNull(string $attribute): self {} - - /** @return static */ - public static function startsWith(string $attribute, string $value): self {} - - /** @return static */ - public static function notStartsWith(string $attribute, string $value): self {} - - /** @return static */ - public static function endsWith(string $attribute, string $value): self {} - - /** @return static */ - public static function notEndsWith(string $attribute, string $value): self {} - - /** @return static */ - public static function createdBefore(string $value): self {} - - /** @return static */ - public static function createdAfter(string $value): self {} - - /** @return static */ - public static function updatedBefore(string $value): self {} - - /** @return static */ - public static function updatedAfter(string $value): self {} - - /** @return static */ - public static function createdBetween(string $start, string $end): self {} - - /** @return static */ - public static function updatedBetween(string $start, string $end): self {} - - /** - * @param array $queries - * @return static - */ - public static function or(array $queries): self {} - - /** - * @param array $queries - * @return static - */ - public static function and(array $queries): self {} - - /** - * @param array $values - * @return static - */ - public static function containsAll(string $attribute, array $values): self {} - - /** - * @param array $queries - * @return static - */ - public static function elemMatch(string $attribute, array $queries): self {} - - /** - * @param array $queries - * @param array $types - * @return array - */ - public static function getByType(array $queries, array $types, bool $clone = true): array {} - - /** - * @param array $queries - * @return array - */ - public static function getCursorQueries(array $queries, bool $clone = true): array {} - - /** - * @param array $queries - * @return array{ - * filters: array, - * selections: array, - * limit: int|null, - * offset: int|null, - * orderAttributes: array, - * orderTypes: array, - * cursor: mixed, - * cursorDirection: string|null - * } - */ - public static function groupByType(array $queries): array {} - - /** @return static */ - public function setMethod(string $method): self {} - - /** @return static */ - public function setAttribute(string $attribute): self {} - - /** - * @param array $values - * @return static - */ - public function setValues(array $values): self {} - - /** @return static */ - public function setValue(mixed $value): self {} - - /** - * @param array $values - * @return static - */ - public static function distanceEqual(string $attribute, array $values, int|float $distance, bool $meters = false): self {} - - /** - * @param array $values - * @return static - */ - public static function distanceNotEqual(string $attribute, array $values, int|float $distance, bool $meters = false): self {} - - /** - * @param array $values - * @return static - */ - public static function distanceGreaterThan(string $attribute, array $values, int|float $distance, bool $meters = false): self {} - - /** - * @param array $values - * @return static - */ - public static function distanceLessThan(string $attribute, array $values, int|float $distance, bool $meters = false): self {} - - /** - * @param array $values - * @return static - */ - public static function intersects(string $attribute, array $values): self {} - - /** - * @param array $values - * @return static - */ - public static function notIntersects(string $attribute, array $values): self {} - - /** - * @param array $values - * @return static - */ - public static function crosses(string $attribute, array $values): self {} - - /** - * @param array $values - * @return static - */ - public static function notCrosses(string $attribute, array $values): self {} - - /** - * @param array $values - * @return static - */ - public static function overlaps(string $attribute, array $values): self {} - - /** - * @param array $values - * @return static - */ - public static function notOverlaps(string $attribute, array $values): self {} - - /** - * @param array $values - * @return static - */ - public static function touches(string $attribute, array $values): self {} - - /** - * @param array $values - * @return static - */ - public static function notTouches(string $attribute, array $values): self {} - - /** - * @param array $vector - * @return static - */ - public static function vectorDot(string $attribute, array $vector): self {} - - /** - * @param array $vector - * @return static - */ - public static function vectorCosine(string $attribute, array $vector): self {} - - /** - * @param array $vector - * @return static - */ - public static function vectorEuclidean(string $attribute, array $vector): self {} - - /** @return static */ - public static function regex(string $attribute, string $pattern): self {} - - /** - * @param array $attributes - * @return static - */ - public static function exists(array $attributes): self {} - - /** - * @param string|int|float|bool|array $attribute - * @return static - */ - public static function notExists(string|int|float|bool|array $attribute): self {} -} From 782ed2d0eace5fa8bc7cb726e4cb0ae4f093badf Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 3 Mar 2026 22:14:09 +1300 Subject: [PATCH 4/4] fix: update utopia-php/query to 0.1.1 for static return types Co-Authored-By: Claude Opus 4.6 --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index ea820e2d8..ae3bf75aa 100644 --- a/composer.lock +++ b/composer.lock @@ -2287,16 +2287,16 @@ }, { "name": "utopia-php/query", - "version": "0.1.0", + "version": "0.1.1", "source": { "type": "git", "url": "https://github.com/utopia-php/query.git", - "reference": "601490f2967f7b628d4fb62994ba39fe119907db" + "reference": "964a10ed3185490505f4c0062f2eb7b89287fb27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/query/zipball/601490f2967f7b628d4fb62994ba39fe119907db", - "reference": "601490f2967f7b628d4fb62994ba39fe119907db", + "url": "https://api.github.com/repos/utopia-php/query/zipball/964a10ed3185490505f4c0062f2eb7b89287fb27", + "reference": "964a10ed3185490505f4c0062f2eb7b89287fb27", "shasum": "" }, "require": { @@ -2344,10 +2344,10 @@ "utopia" ], "support": { - "source": "https://github.com/utopia-php/query/tree/0.1.0", + "source": "https://github.com/utopia-php/query/tree/0.1.1", "issues": "https://github.com/utopia-php/query/issues" }, - "time": "2026-03-03T07:49:53+00:00" + "time": "2026-03-03T09:05:14+00:00" }, { "name": "utopia-php/telemetry",