diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1a4a0d3a6..90a0abd89 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -62,8 +62,12 @@ jobs: fetch-depth: 2 submodules: true + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - id: setup-mongodb - uses: mongodb-labs/drivers-evergreen-tools@81c6b49dd61e833229d750084f9f3c71b31acd8d + uses: mongodb-labs/drivers-evergreen-tools@master with: version: ${{ matrix.mongodb-version }} topology: ${{ matrix.topology }} diff --git a/generator/config/stage/geoNear.yaml b/generator/config/stage/geoNear.yaml index c9968509d..699f15146 100644 --- a/generator/config/stage/geoNear.yaml +++ b/generator/config/stage/geoNear.yaml @@ -11,6 +11,7 @@ arguments: name: distanceField type: - string + optional: true description: | The output field that contains the calculated distance. To specify a field within an embedded document, use dot notation. - diff --git a/src/Builder/Stage/FactoryTrait.php b/src/Builder/Stage/FactoryTrait.php index 7ef8fb62b..983da9e09 100644 --- a/src/Builder/Stage/FactoryTrait.php +++ b/src/Builder/Stage/FactoryTrait.php @@ -248,8 +248,8 @@ public static function fill( * Returns an ordered stream of documents based on the proximity to a geospatial point. Incorporates the functionality of $match, $sort, and $limit for geospatial data. The output documents include an additional distance field and can include a location identifier field. * * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/geoNear/ - * @param string $distanceField The output field that contains the calculated distance. To specify a field within an embedded document, use dot notation. * @param Document|ResolvesToObject|Serializable|array|stdClass $near The point for which to find the closest documents. + * @param Optional|string $distanceField The output field that contains the calculated distance. To specify a field within an embedded document, use dot notation. * @param Optional|Decimal128|Int64|float|int $distanceMultiplier The factor to multiply all distances returned by the query. For example, use the distanceMultiplier to convert radians, as returned by a spherical query, to kilometers by multiplying by the radius of the Earth. * @param Optional|string $includeLocs This specifies the output field that identifies the location used to calculate the distance. This option is useful when a location field contains multiple locations. To specify a field within an embedded document, use dot notation. * @param Optional|string $key Specify the geospatial indexed field to use when calculating the distance. @@ -265,8 +265,8 @@ public static function fill( * Default: false. */ public static function geoNear( - string $distanceField, Document|Serializable|ResolvesToObject|stdClass|array $near, + Optional|string $distanceField = Optional::Undefined, Optional|Decimal128|Int64|float|int $distanceMultiplier = Optional::Undefined, Optional|string $includeLocs = Optional::Undefined, Optional|string $key = Optional::Undefined, @@ -275,7 +275,7 @@ public static function geoNear( Optional|QueryInterface|array $query = Optional::Undefined, Optional|bool $spherical = Optional::Undefined, ): GeoNearStage { - return new GeoNearStage($distanceField, $near, $distanceMultiplier, $includeLocs, $key, $maxDistance, $minDistance, $query, $spherical); + return new GeoNearStage($near, $distanceField, $distanceMultiplier, $includeLocs, $key, $maxDistance, $minDistance, $query, $spherical); } /** diff --git a/src/Builder/Stage/FluentFactoryTrait.php b/src/Builder/Stage/FluentFactoryTrait.php index 803ca9905..0d6e981ea 100644 --- a/src/Builder/Stage/FluentFactoryTrait.php +++ b/src/Builder/Stage/FluentFactoryTrait.php @@ -295,8 +295,8 @@ public function fill( * Returns an ordered stream of documents based on the proximity to a geospatial point. Incorporates the functionality of $match, $sort, and $limit for geospatial data. The output documents include an additional distance field and can include a location identifier field. * * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/geoNear/ - * @param string $distanceField The output field that contains the calculated distance. To specify a field within an embedded document, use dot notation. * @param Document|ResolvesToObject|Serializable|array|stdClass $near The point for which to find the closest documents. + * @param Optional|string $distanceField The output field that contains the calculated distance. To specify a field within an embedded document, use dot notation. * @param Optional|Decimal128|Int64|float|int $distanceMultiplier The factor to multiply all distances returned by the query. For example, use the distanceMultiplier to convert radians, as returned by a spherical query, to kilometers by multiplying by the radius of the Earth. * @param Optional|string $includeLocs This specifies the output field that identifies the location used to calculate the distance. This option is useful when a location field contains multiple locations. To specify a field within an embedded document, use dot notation. * @param Optional|string $key Specify the geospatial indexed field to use when calculating the distance. @@ -312,8 +312,8 @@ public function fill( * Default: false. */ public function geoNear( - string $distanceField, Document|Serializable|ResolvesToObject|stdClass|array $near, + Optional|string $distanceField = Optional::Undefined, Optional|Decimal128|Int64|int|float $distanceMultiplier = Optional::Undefined, Optional|string $includeLocs = Optional::Undefined, Optional|string $key = Optional::Undefined, @@ -322,7 +322,7 @@ public function geoNear( Optional|QueryInterface|array $query = Optional::Undefined, Optional|bool $spherical = Optional::Undefined, ): static { - $this->pipeline[] = Stage::geoNear($distanceField, $near, $distanceMultiplier, $includeLocs, $key, $maxDistance, $minDistance, $query, $spherical); + $this->pipeline[] = Stage::geoNear($near, $distanceField, $distanceMultiplier, $includeLocs, $key, $maxDistance, $minDistance, $query, $spherical); return $this; } diff --git a/src/Builder/Stage/GeoNearStage.php b/src/Builder/Stage/GeoNearStage.php index b2aa6588e..845fd2afa 100644 --- a/src/Builder/Stage/GeoNearStage.php +++ b/src/Builder/Stage/GeoNearStage.php @@ -35,8 +35,8 @@ final class GeoNearStage implements StageInterface, OperatorInterface public const NAME = '$geoNear'; public const PROPERTIES = [ - 'distanceField' => 'distanceField', 'near' => 'near', + 'distanceField' => 'distanceField', 'distanceMultiplier' => 'distanceMultiplier', 'includeLocs' => 'includeLocs', 'key' => 'key', @@ -46,12 +46,12 @@ final class GeoNearStage implements StageInterface, OperatorInterface 'spherical' => 'spherical', ]; - /** @var string $distanceField The output field that contains the calculated distance. To specify a field within an embedded document, use dot notation. */ - public readonly string $distanceField; - /** @var Document|ResolvesToObject|Serializable|array|stdClass $near The point for which to find the closest documents. */ public readonly Document|Serializable|ResolvesToObject|stdClass|array $near; + /** @var Optional|string $distanceField The output field that contains the calculated distance. To specify a field within an embedded document, use dot notation. */ + public readonly Optional|string $distanceField; + /** @var Optional|Decimal128|Int64|float|int $distanceMultiplier The factor to multiply all distances returned by the query. For example, use the distanceMultiplier to convert radians, as returned by a spherical query, to kilometers by multiplying by the radius of the Earth. */ public readonly Optional|Decimal128|Int64|float|int $distanceMultiplier; @@ -88,8 +88,8 @@ final class GeoNearStage implements StageInterface, OperatorInterface public readonly Optional|bool $spherical; /** - * @param string $distanceField The output field that contains the calculated distance. To specify a field within an embedded document, use dot notation. * @param Document|ResolvesToObject|Serializable|array|stdClass $near The point for which to find the closest documents. + * @param Optional|string $distanceField The output field that contains the calculated distance. To specify a field within an embedded document, use dot notation. * @param Optional|Decimal128|Int64|float|int $distanceMultiplier The factor to multiply all distances returned by the query. For example, use the distanceMultiplier to convert radians, as returned by a spherical query, to kilometers by multiplying by the radius of the Earth. * @param Optional|string $includeLocs This specifies the output field that identifies the location used to calculate the distance. This option is useful when a location field contains multiple locations. To specify a field within an embedded document, use dot notation. * @param Optional|string $key Specify the geospatial indexed field to use when calculating the distance. @@ -105,8 +105,8 @@ final class GeoNearStage implements StageInterface, OperatorInterface * Default: false. */ public function __construct( - string $distanceField, Document|Serializable|ResolvesToObject|stdClass|array $near, + Optional|string $distanceField = Optional::Undefined, Optional|Decimal128|Int64|float|int $distanceMultiplier = Optional::Undefined, Optional|string $includeLocs = Optional::Undefined, Optional|string $key = Optional::Undefined, @@ -115,8 +115,8 @@ public function __construct( Optional|QueryInterface|array $query = Optional::Undefined, Optional|bool $spherical = Optional::Undefined, ) { - $this->distanceField = $distanceField; $this->near = $near; + $this->distanceField = $distanceField; $this->distanceMultiplier = $distanceMultiplier; $this->includeLocs = $includeLocs; $this->key = $key; diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php index 115f6a5c7..964b259c1 100644 --- a/src/Exception/InvalidArgumentException.php +++ b/src/Exception/InvalidArgumentException.php @@ -34,6 +34,17 @@ public static function cannotCombineCodecAndTypeMap(): self return new self('Cannot provide both "codec" and "typeMap" options'); } + /** + * Thrown when an argument or option is expected to be a string or a document. + * + * @param string $name Name of the argument or option + * @param mixed $value Actual value (used to derive the type) + */ + public static function expectedDocumentOrStringType(string $name, mixed $value): self + { + return new self(sprintf('Expected %s to have type "string" or "document" (array or object) but found "%s"', $name, get_debug_type($value))); + } + /** * Thrown when an argument or option is expected to be a document. * diff --git a/src/Operation/Aggregate.php b/src/Operation/Aggregate.php index 3543fc73c..7c968a250 100644 --- a/src/Operation/Aggregate.php +++ b/src/Operation/Aggregate.php @@ -37,7 +37,6 @@ use function is_array; use function is_bool; use function is_integer; -use function is_object; use function is_string; use function MongoDB\is_document; use function MongoDB\is_last_pipeline_operator_write; @@ -150,8 +149,8 @@ public function __construct(private string $databaseName, private ?string $colle throw InvalidArgumentException::invalidType('"explain" option', $this->options['explain'], 'boolean'); } - if (isset($this->options['hint']) && ! is_string($this->options['hint']) && ! is_array($this->options['hint']) && ! is_object($this->options['hint'])) { - throw InvalidArgumentException::invalidType('"hint" option', $this->options['hint'], 'string or array or object'); + if (isset($this->options['hint']) && ! is_string($this->options['hint']) && ! is_document($this->options['hint'])) { + throw InvalidArgumentException::expectedDocumentOrStringType('"hint" option', $this->options['hint']); } if (isset($this->options['let']) && ! is_document($this->options['let'])) { diff --git a/src/Operation/Count.php b/src/Operation/Count.php index 9f9225942..f3d2b7eb6 100644 --- a/src/Operation/Count.php +++ b/src/Operation/Count.php @@ -90,8 +90,8 @@ public function __construct(private string $databaseName, private string $collec throw InvalidArgumentException::expectedDocumentType('"collation" option', $this->options['collation']); } - if (isset($this->options['hint']) && ! is_string($this->options['hint']) && ! is_array($this->options['hint']) && ! is_object($this->options['hint'])) { - throw InvalidArgumentException::invalidType('"hint" option', $this->options['hint'], 'string or array or object'); + if (isset($this->options['hint']) && ! is_string($this->options['hint']) && ! is_document($this->options['hint'])) { + throw InvalidArgumentException::expectedDocumentOrStringType('"hint" option', $this->options['hint']); } if (isset($this->options['limit']) && ! is_integer($this->options['limit'])) { diff --git a/src/Operation/Delete.php b/src/Operation/Delete.php index 477cade91..c5826a6d9 100644 --- a/src/Operation/Delete.php +++ b/src/Operation/Delete.php @@ -26,8 +26,6 @@ use MongoDB\Exception\InvalidArgumentException; use MongoDB\Exception\UnsupportedException; -use function is_array; -use function is_object; use function is_string; use function MongoDB\is_document; use function MongoDB\is_write_concern_acknowledged; @@ -96,8 +94,8 @@ public function __construct(private string $databaseName, private string $collec throw InvalidArgumentException::expectedDocumentType('"collation" option', $this->options['collation']); } - if (isset($this->options['hint']) && ! is_string($this->options['hint']) && ! is_array($this->options['hint']) && ! is_object($this->options['hint'])) { - throw InvalidArgumentException::invalidType('"hint" option', $this->options['hint'], ['string', 'array', 'object']); + if (isset($this->options['hint']) && ! is_string($this->options['hint']) && ! is_document($this->options['hint'])) { + throw InvalidArgumentException::expectedDocumentOrStringType('"hint" option', $this->options['hint']); } if (isset($this->options['session']) && ! $this->options['session'] instanceof Session) { diff --git a/src/Operation/Distinct.php b/src/Operation/Distinct.php index a979df229..bd69cde11 100644 --- a/src/Operation/Distinct.php +++ b/src/Operation/Distinct.php @@ -91,7 +91,7 @@ public function __construct(private string $databaseName, private string $collec } if (isset($this->options['hint']) && ! is_string($this->options['hint']) && ! is_document($this->options['hint'])) { - throw InvalidArgumentException::invalidType('"hint" option', $this->options['hint'], 'string or array or object'); + throw InvalidArgumentException::expectedDocumentOrStringType('"hint" option', $this->options['hint']); } if (isset($this->options['maxTimeMS']) && ! is_integer($this->options['maxTimeMS'])) { diff --git a/src/Operation/Find.php b/src/Operation/Find.php index 6516d52c9..a4a010010 100644 --- a/src/Operation/Find.php +++ b/src/Operation/Find.php @@ -195,8 +195,8 @@ public function __construct(private string $databaseName, private string $collec } } - if (isset($this->options['hint']) && ! is_string($this->options['hint']) && ! is_array($this->options['hint']) && ! is_object($this->options['hint'])) { - throw InvalidArgumentException::invalidType('"hint" option', $this->options['hint'], 'string or array or object'); + if (isset($this->options['hint']) && ! is_string($this->options['hint']) && ! is_document($this->options['hint'])) { + throw InvalidArgumentException::expectedDocumentOrStringType('"hint" option', $this->options['hint']); } if (isset($this->options['limit']) && ! is_integer($this->options['limit'])) { diff --git a/src/Operation/FindAndModify.php b/src/Operation/FindAndModify.php index e67413b6c..07944db3d 100644 --- a/src/Operation/FindAndModify.php +++ b/src/Operation/FindAndModify.php @@ -151,8 +151,8 @@ public function __construct(private string $databaseName, private string $collec throw InvalidArgumentException::expectedDocumentType('"fields" option', $options['fields']); } - if (isset($options['hint']) && ! is_string($options['hint']) && ! is_array($options['hint']) && ! is_object($options['hint'])) { - throw InvalidArgumentException::invalidType('"hint" option', $options['hint'], ['string', 'array', 'object']); + if (isset($options['hint']) && ! is_string($options['hint']) && ! is_document($options['hint'])) { + throw InvalidArgumentException::expectedDocumentOrStringType('"hint" option', $options['hint']); } if (isset($options['maxTimeMS']) && ! is_integer($options['maxTimeMS'])) { diff --git a/src/Operation/Update.php b/src/Operation/Update.php index 0b47b1cc1..5d011da28 100644 --- a/src/Operation/Update.php +++ b/src/Operation/Update.php @@ -28,7 +28,6 @@ use function is_array; use function is_bool; -use function is_object; use function is_string; use function MongoDB\is_document; use function MongoDB\is_first_key_operator; @@ -122,8 +121,8 @@ public function __construct(private string $databaseName, private string $collec throw InvalidArgumentException::expectedDocumentType('"collation" option', $options['collation']); } - if (isset($options['hint']) && ! is_string($options['hint']) && ! is_array($options['hint']) && ! is_object($options['hint'])) { - throw InvalidArgumentException::invalidType('"hint" option', $options['hint'], ['string', 'array', 'object']); + if (isset($options['hint']) && ! is_string($options['hint']) && ! is_document($options['hint'])) { + throw InvalidArgumentException::expectedDocumentOrStringType('"hint" option', $options['hint']); } if (! is_bool($options['multi'])) { diff --git a/tests/TestCase.php b/tests/TestCase.php index c8c7db09d..2b6b86eb2 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -250,7 +250,7 @@ protected static function getInvalidDocumentCodecValues(): array */ protected static function getInvalidHintValues(): array { - return [123, 3.14, true]; + return [123, 3.14, true, PackedArray::fromPHP([])]; } /** diff --git a/tests/specifications b/tests/specifications index ce35696db..60fe09fc1 160000 --- a/tests/specifications +++ b/tests/specifications @@ -1 +1 @@ -Subproject commit ce35696db1c8da0e15a067b8022c853f1d6d1292 +Subproject commit 60fe09fc17a1963d683e904eb8b43f6675d77a1a