diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 74f7c0b..872605b 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -42,7 +42,6 @@
-
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index f4acb63..a8793bf 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -1,3 +1,134 @@
-
+
+
+
+ $val
+ $val
+ $value[$key]
+
+
+
+
+ $filter
+ $filterValue
+
+
+
+
+ name]]>
+
+
+
+
+ $result
+ $result[]
+ $val
+ $val
+ $val
+ $val
+ $val
+
+
+
+
+ $result
+
+
+
+
+ recursiveEncode($value)]]>
+
+
+ $subValue
+ $value
+
+
+
+
+ $query
+
+
+
+
+ $query
+
+
+
+
+ $query
+
+
+
+
+ $expression
+
+
+ stdClass
+
+
+
+
+ $facet
+
+
+ stdClass
+
+
+
+
+ $query
+
+
+
+
+ $restrictSearchWithMatch
+
+
+
+
+ $field
+
+
+ stdClass
+
+
+
+
+ $query
+
+
+
+
+ $specification
+
+
+ stdClass
+
+
+
+
+ $field
+
+
+ stdClass
+
+
+
+
+
+
+
+
+
+
+ $queries[$fieldPath]
+ $query
+
+
+ 0]]>
+
+
diff --git a/src/Builder/BuilderEncoder.php b/src/Builder/BuilderEncoder.php
index c7c0b27..8cc9699 100644
--- a/src/Builder/BuilderEncoder.php
+++ b/src/Builder/BuilderEncoder.php
@@ -4,323 +4,98 @@
namespace MongoDB\Builder;
-use LogicException;
+use MongoDB\Builder\Encoder\CombinedFieldQueryEncoder;
+use MongoDB\Builder\Encoder\ExpressionEncoder;
+use MongoDB\Builder\Encoder\FieldPathEncoder;
+use MongoDB\Builder\Encoder\OperatorEncoder;
+use MongoDB\Builder\Encoder\OutputWindowEncoder;
+use MongoDB\Builder\Encoder\PipelineEncoder;
+use MongoDB\Builder\Encoder\QueryEncoder;
+use MongoDB\Builder\Encoder\VariableEncoder;
use MongoDB\Builder\Expression\Variable;
-use MongoDB\Builder\Stage\GroupStage;
-use MongoDB\Builder\Type\AccumulatorInterface;
use MongoDB\Builder\Type\CombinedFieldQuery;
-use MongoDB\Builder\Type\Encode;
use MongoDB\Builder\Type\ExpressionInterface;
use MongoDB\Builder\Type\FieldPathInterface;
-use MongoDB\Builder\Type\FieldQueryInterface;
use MongoDB\Builder\Type\OperatorInterface;
-use MongoDB\Builder\Type\Optional;
use MongoDB\Builder\Type\OutputWindow;
-use MongoDB\Builder\Type\ProjectionInterface;
use MongoDB\Builder\Type\QueryInterface;
use MongoDB\Builder\Type\QueryObject;
use MongoDB\Builder\Type\StageInterface;
-use MongoDB\Builder\Type\WindowInterface;
use MongoDB\Codec\EncodeIfSupported;
use MongoDB\Codec\Encoder;
use MongoDB\Exception\UnsupportedValueException;
use stdClass;
use function array_key_exists;
-use function array_key_first;
-use function assert;
-use function get_debug_type;
-use function get_object_vars;
-use function is_array;
use function is_object;
-use function MongoDB\is_first_key_operator;
-use function property_exists;
-use function sprintf;
-/** @template-implements Encoder */
+/** @template-implements Encoder */
class BuilderEncoder implements Encoder
{
+ /** @template-use EncodeIfSupported */
use EncodeIfSupported;
- /**
- * {@inheritdoc}
- */
- public function canEncode($value): bool
+ /** @var array> */
+ private array $defaultEncoders = [
+ Pipeline::class => PipelineEncoder::class,
+ Variable::class => VariableEncoder::class,
+ FieldPathInterface::class => FieldPathEncoder::class,
+ CombinedFieldQuery::class => CombinedFieldQueryEncoder::class,
+ QueryObject::class => QueryEncoder::class,
+ OutputWindow::class => OutputWindowEncoder::class,
+ OperatorInterface::class => OperatorEncoder::class,
+ ];
+
+ /** @var array */
+ private array $cachedEncoders = [];
+
+ /** @param array> $customEncoders */
+ public function __construct(private readonly array $customEncoders = [])
{
- return $value instanceof Pipeline
- || $value instanceof OperatorInterface
- || $value instanceof ExpressionInterface
- || $value instanceof QueryInterface
- || $value instanceof FieldQueryInterface
- || $value instanceof AccumulatorInterface
- || $value instanceof ProjectionInterface
- || $value instanceof WindowInterface;
}
- /**
- * {@inheritdoc}
- */
- public function encode($value): stdClass|array|string
+ /** @psalm-assert-if-true object $value */
+ public function canEncode(mixed $value): bool
{
- if (! $this->canEncode($value)) {
- throw UnsupportedValueException::invalidEncodableValue($value);
- }
-
- // A pipeline is encoded as a list of stages
- if ($value instanceof Pipeline) {
- $encoded = [];
- foreach ($value->getIterator() as $stage) {
- $encoded[] = $this->encodeIfSupported($stage);
- }
-
- return $encoded;
- }
-
- // This specific encoding code if temporary until we have a generic way to encode stages and operators
- if ($value instanceof FieldPathInterface) {
- return '$' . $value->name;
- }
-
- if ($value instanceof Variable) {
- return '$$' . $value->name;
- }
-
- if ($value instanceof QueryObject) {
- return $this->encodeQueryObject($value);
- }
-
- if ($value instanceof CombinedFieldQuery) {
- return $this->encodeCombinedFilter($value);
- }
-
- if ($value instanceof OutputWindow) {
- return $this->encodeOutputWindow($value);
- }
-
- if (! $value instanceof OperatorInterface) {
- throw new LogicException(sprintf('Class "%s" does not implement OperatorInterface.', $value::class));
- }
-
- // The generic but incomplete encoding code
- switch ($value::ENCODE) {
- case Encode::Single:
- return $this->encodeAsSingle($value);
-
- case Encode::Array:
- return $this->encodeAsArray($value);
-
- case Encode::Object:
- return $this->encodeAsObject($value);
-
- case Encode::DollarObject:
- return $this->encodeAsDollarObject($value);
-
- case Encode::Group:
- assert($value instanceof GroupStage);
-
- return $this->encodeAsGroup($value);
- }
-
- throw new LogicException(sprintf('Class "%s" does not have a valid ENCODE constant.', $value::class));
- }
-
- /**
- * Encode the value as an array of properties, in the order they are defined in the class.
- */
- private function encodeAsArray(OperatorInterface $value): stdClass
- {
- $result = [];
- /** @var mixed $val */
- foreach (get_object_vars($value) as $val) {
- // Skip optional arguments.
- // $slice operator has the optional argument in the middle of the array
- if ($val === Optional::Undefined) {
- continue;
- }
-
- $result[] = $this->recursiveEncode($val);
- }
-
- return $this->wrap($value, $result);
- }
-
- /**
- * $group stage have a specific encoding because the _id argument is required and others are variadic
- */
- private function encodeAsGroup(GroupStage $value): stdClass
- {
- $result = new stdClass();
- $result->_id = $this->recursiveEncode($value->_id);
-
- foreach (get_object_vars($value->field) as $key => $val) {
- $result->{$key} = $this->recursiveEncode($val);
- }
-
- return $this->wrap($value, $result);
- }
-
- private function encodeAsObject(OperatorInterface $value): stdClass
- {
- $result = new stdClass();
- foreach (get_object_vars($value) as $key => $val) {
- // Skip optional arguments. If they have a default value, it is resolved by the server.
- if ($val === Optional::Undefined) {
- continue;
- }
-
- $result->{$key} = $this->recursiveEncode($val);
- }
-
- return $this->wrap($value, $result);
- }
-
- private function encodeAsDollarObject(OperatorInterface $value): stdClass
- {
- $result = new stdClass();
- foreach (get_object_vars($value) as $key => $val) {
- // Skip optional arguments. If they have a default value, it is resolved by the server.
- if ($val === Optional::Undefined) {
- continue;
- }
-
- $val = $this->recursiveEncode($val);
-
- if ($key === 'geometry') {
- if (is_object($val) && property_exists($val, '$geometry')) {
- $result->{'$geometry'} = $val->{'$geometry'};
- } elseif (is_array($val) && array_key_exists('$geometry', $val)) {
- $result->{'$geometry'} = $val->{'$geometry'};
- } else {
- $result->{'$geometry'} = $val;
- }
- } else {
- $result->{'$' . $key} = $val;
- }
- }
-
- return $this->wrap($value, $result);
- }
-
- /**
- * Get the unique property of the operator as value
- */
- private function encodeAsSingle(OperatorInterface $value): stdClass
- {
- foreach (get_object_vars($value) as $val) {
- $result = $this->recursiveEncode($val);
-
- return $this->wrap($value, $result);
+ if (! is_object($value)) {
+ return false;
}
- throw new LogicException(sprintf('Class "%s" does not have a single property.', $value::class));
+ return (bool) $this->getEncoderFor($value)?->canEncode($value);
}
- private function encodeCombinedFilter(CombinedFieldQuery $filter): stdClass
+ public function encode(mixed $value): stdClass|array|string
{
- $result = new stdClass();
- foreach ($filter->fieldQueries as $filter) {
- $filter = $this->recursiveEncode($filter);
- if (is_object($filter)) {
- $filter = get_object_vars($filter);
- } elseif (! is_array($filter)) {
- throw new LogicException(sprintf('Query filters must an array or an object. Got "%s"', get_debug_type($filter)));
- }
-
- foreach ($filter as $key => $value) {
- $result->{$key} = $value;
- }
- }
-
- return $result;
- }
+ $encoder = $this->getEncoderFor($value);
- /**
- * Query objects are encoded by merging query operator with field path to filter operators in the same object.
- */
- private function encodeQueryObject(QueryObject $query): stdClass
- {
- $result = new stdClass();
- foreach ($query->queries as $key => $value) {
- if ($value instanceof QueryInterface) {
- // The sub-objects is merged into the main object, replacing duplicate keys
- foreach (get_object_vars($this->recursiveEncode($value)) as $subKey => $subValue) {
- if (property_exists($result, (string) $subKey)) {
- throw new LogicException(sprintf('Duplicate key "%s" in query object', $subKey));
- }
-
- $result->{$subKey} = $subValue;
- }
- } else {
- if (property_exists($result, (string) $key)) {
- throw new LogicException(sprintf('Duplicate key "%s" in query object', $key));
- }
-
- $result->{$key} = $this->encodeIfSupported($value);
- }
+ if (! $encoder?->canEncode($value)) {
+ throw UnsupportedValueException::invalidEncodableValue($value);
}
- return $result;
+ return $encoder->encode($value);
}
- /**
- * For the $setWindowFields stage output parameter, the optional window parameter is encoded in the same object
- * of the window operator.
- *
- * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/setWindowFields/
- */
- private function encodeOutputWindow(OutputWindow $outputWindow): stdClass
+ private function getEncoderFor(object $value): ExpressionEncoder|null
{
- $result = $this->recursiveEncode($outputWindow->operator);
-
- // Transform the result into an stdClass if a document is provided
- if (! $outputWindow->operator instanceof WindowInterface && (is_array($result) || is_object($result))) {
- if (! is_first_key_operator($result)) {
- throw new LogicException(sprintf('Expected OutputWindow::$operator to be an operator. Got "%s"', array_key_first((array) $result)));
- }
-
- $result = (object) $result;
+ $valueClass = $value::class;
+ if (array_key_exists($valueClass, $this->cachedEncoders)) {
+ return $this->cachedEncoders[$valueClass];
}
- if (! $result instanceof stdClass) {
- throw new LogicException(sprintf('Expected OutputWindow::$operator to be an stdClass, array or WindowInterface. Got "%s"', get_debug_type($result)));
- }
+ $encoderList = $this->customEncoders + $this->defaultEncoders;
- if ($outputWindow->window !== Optional::Undefined) {
- $result->window = $this->recursiveEncode($outputWindow->window);
+ // First attempt: match class name exactly
+ if (isset($encoderList[$valueClass])) {
+ return $this->cachedEncoders[$valueClass] = new $encoderList[$valueClass]($this);
}
- return $result;
- }
-
- /**
- * Nested arrays and objects must be encoded recursively.
- */
- private function recursiveEncode(mixed $value): mixed
- {
- if (is_array($value)) {
- foreach ($value as $key => $val) {
- $value[$key] = $this->recursiveEncode($val);
- }
-
- return $value;
- }
-
- if ($value instanceof stdClass) {
- foreach (get_object_vars($value) as $key => $val) {
- $value->{$key} = $this->recursiveEncode($val);
+ // Second attempt: catch child classes
+ foreach ($encoderList as $className => $encoderClass) {
+ if ($value instanceof $className) {
+ return $this->cachedEncoders[$valueClass] = new $encoderClass($this);
}
-
- return $value;
}
- return $this->encodeIfSupported($value);
- }
-
- private function wrap(OperatorInterface $value, mixed $result): stdClass
- {
- $object = new stdClass();
- $object->{$value->getOperator()} = $result;
-
- return $object;
+ return $this->cachedEncoders[$valueClass] = null;
}
}
diff --git a/src/Builder/Encoder/AbstractExpressionEncoder.php b/src/Builder/Encoder/AbstractExpressionEncoder.php
new file mode 100644
index 0000000..8bbdbae
--- /dev/null
+++ b/src/Builder/Encoder/AbstractExpressionEncoder.php
@@ -0,0 +1,53 @@
+
+ */
+abstract class AbstractExpressionEncoder implements ExpressionEncoder
+{
+ final public function __construct(protected readonly BuilderEncoder $encoder)
+ {
+ }
+
+ /**
+ * Nested arrays and objects must be encoded recursively.
+ *
+ * @psalm-param T $value
+ *
+ * @psalm-return (T is stdClass ? stdClass : (T is array ? array : mixed))
+ *
+ * @template T
+ */
+ final protected function recursiveEncode(mixed $value): mixed
+ {
+ if (is_array($value)) {
+ foreach ($value as $key => $val) {
+ $value[$key] = $this->recursiveEncode($val);
+ }
+
+ return $value;
+ }
+
+ if ($value instanceof stdClass) {
+ foreach (get_object_vars($value) as $key => $val) {
+ $value->{$key} = $this->recursiveEncode($val);
+ }
+
+ return $value;
+ }
+
+ return $this->encoder->encodeIfSupported($value);
+ }
+}
diff --git a/src/Builder/Encoder/CombinedFieldQueryEncoder.php b/src/Builder/Encoder/CombinedFieldQueryEncoder.php
new file mode 100644
index 0000000..4ad4239
--- /dev/null
+++ b/src/Builder/Encoder/CombinedFieldQueryEncoder.php
@@ -0,0 +1,52 @@
+ */
+class CombinedFieldQueryEncoder extends AbstractExpressionEncoder
+{
+ /** @template-use EncodeIfSupported */
+ use EncodeIfSupported;
+
+ public function canEncode(mixed $value): bool
+ {
+ return $value instanceof CombinedFieldQuery;
+ }
+
+ public function encode(mixed $value): stdClass
+ {
+ if (! $this->canEncode($value)) {
+ throw UnsupportedValueException::invalidEncodableValue($value);
+ }
+
+ $result = new stdClass();
+ foreach ($value->fieldQueries as $filter) {
+ $filter = $this->recursiveEncode($filter);
+ if (is_object($filter)) {
+ $filter = get_object_vars($filter);
+ } elseif (! is_array($filter)) {
+ throw new LogicException(sprintf('Query filters must an array or an object. Got "%s"', get_debug_type($filter)));
+ }
+
+ foreach ($filter as $key => $filterValue) {
+ $result->{$key} = $filterValue;
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/src/Builder/Encoder/ExpressionEncoder.php b/src/Builder/Encoder/ExpressionEncoder.php
new file mode 100644
index 0000000..6613c45
--- /dev/null
+++ b/src/Builder/Encoder/ExpressionEncoder.php
@@ -0,0 +1,19 @@
+
+ */
+interface ExpressionEncoder extends Encoder
+{
+ public function __construct(BuilderEncoder $encoder);
+}
diff --git a/src/Builder/Encoder/FieldPathEncoder.php b/src/Builder/Encoder/FieldPathEncoder.php
new file mode 100644
index 0000000..8fe7381
--- /dev/null
+++ b/src/Builder/Encoder/FieldPathEncoder.php
@@ -0,0 +1,31 @@
+ */
+class FieldPathEncoder extends AbstractExpressionEncoder
+{
+ /** @template-use EncodeIfSupported */
+ use EncodeIfSupported;
+
+ public function canEncode(mixed $value): bool
+ {
+ return $value instanceof FieldPathInterface;
+ }
+
+ public function encode(mixed $value): string
+ {
+ if (! $this->canEncode($value)) {
+ throw UnsupportedValueException::invalidEncodableValue($value);
+ }
+
+ // TODO: needs method because interfaces can't have properties
+ return '$' . $value->name;
+ }
+}
diff --git a/src/Builder/Encoder/OperatorEncoder.php b/src/Builder/Encoder/OperatorEncoder.php
new file mode 100644
index 0000000..4561f22
--- /dev/null
+++ b/src/Builder/Encoder/OperatorEncoder.php
@@ -0,0 +1,161 @@
+ */
+class OperatorEncoder extends AbstractExpressionEncoder
+{
+ /** @template-use EncodeIfSupported */
+ use EncodeIfSupported;
+
+ public function canEncode(mixed $value): bool
+ {
+ return $value instanceof OperatorInterface;
+ }
+
+ public function encode(mixed $value): stdClass
+ {
+ if (! $this->canEncode($value)) {
+ throw UnsupportedValueException::invalidEncodableValue($value);
+ }
+
+ switch ($value::ENCODE) {
+ case Encode::Single:
+ return $this->encodeAsSingle($value);
+
+ case Encode::Array:
+ return $this->encodeAsArray($value);
+
+ case Encode::Object:
+ return $this->encodeAsObject($value);
+
+ case Encode::DollarObject:
+ return $this->encodeAsDollarObject($value);
+
+ case Encode::Group:
+ assert($value instanceof GroupStage);
+
+ return $this->encodeAsGroup($value);
+ }
+
+ throw new LogicException(sprintf('Class "%s" does not have a valid ENCODE constant.', $value::class));
+ }
+
+ /**
+ * Encode the value as an array of properties, in the order they are defined in the class.
+ */
+ private function encodeAsArray(OperatorInterface $value): stdClass
+ {
+ $result = [];
+ /** @var mixed $val */
+ foreach (get_object_vars($value) as $val) {
+ // Skip optional arguments. For example, the $slice expression operator has an optional argument
+ // in the middle of the array.
+ if ($val === Optional::Undefined) {
+ continue;
+ }
+
+ $result[] = $this->recursiveEncode($val);
+ }
+
+ return $this->wrap($value, $result);
+ }
+
+ /**
+ * $group stage have a specific encoding because the _id argument is required and others are variadic
+ */
+ private function encodeAsGroup(GroupStage $value): stdClass
+ {
+ $result = new stdClass();
+ $result->_id = $this->recursiveEncode($value->_id);
+
+ foreach (get_object_vars($value->field) as $key => $val) {
+ $result->{$key} = $this->recursiveEncode($val);
+ }
+
+ return $this->wrap($value, $result);
+ }
+
+ private function encodeAsObject(OperatorInterface $value): stdClass
+ {
+ $result = new stdClass();
+ foreach (get_object_vars($value) as $key => $val) {
+ // Skip optional arguments. If they have a default value, it is resolved by the server.
+ if ($val === Optional::Undefined) {
+ continue;
+ }
+
+ $result->{$key} = $this->recursiveEncode($val);
+ }
+
+ return $this->wrap($value, $result);
+ }
+
+ private function encodeAsDollarObject(OperatorInterface $value): stdClass
+ {
+ $result = new stdClass();
+ foreach (get_object_vars($value) as $key => $val) {
+ // Skip optional arguments. If they have a default value, it is resolved by the server.
+ if ($val === Optional::Undefined) {
+ continue;
+ }
+
+ $val = $this->recursiveEncode($val);
+
+ if ($key === 'geometry') {
+ if (is_object($val) && property_exists($val, '$geometry')) {
+ $result->{'$geometry'} = $val->{'$geometry'};
+ } elseif (is_array($val) && array_key_exists('$geometry', $val)) {
+ $result->{'$geometry'} = $val['$geometry'];
+ } else {
+ $result->{'$geometry'} = $val;
+ }
+ } else {
+ $result->{'$' . $key} = $val;
+ }
+ }
+
+ return $this->wrap($value, $result);
+ }
+
+ /**
+ * Get the unique property of the operator as value
+ */
+ private function encodeAsSingle(OperatorInterface $value): stdClass
+ {
+ foreach (get_object_vars($value) as $val) {
+ $result = $this->recursiveEncode($val);
+
+ return $this->wrap($value, $result);
+ }
+
+ throw new LogicException(sprintf('Class "%s" does not have a single property.', $value::class));
+ }
+
+ private function wrap(OperatorInterface $value, mixed $result): stdClass
+ {
+ $object = new stdClass();
+ $object->{$value->getOperator()} = $result;
+
+ return $object;
+ }
+}
diff --git a/src/Builder/Encoder/OutputWindowEncoder.php b/src/Builder/Encoder/OutputWindowEncoder.php
new file mode 100644
index 0000000..9db5673
--- /dev/null
+++ b/src/Builder/Encoder/OutputWindowEncoder.php
@@ -0,0 +1,60 @@
+ */
+class OutputWindowEncoder extends AbstractExpressionEncoder
+{
+ /** @template-use EncodeIfSupported */
+ use EncodeIfSupported;
+
+ public function canEncode(mixed $value): bool
+ {
+ return $value instanceof OutputWindow;
+ }
+
+ public function encode(mixed $value): stdClass
+ {
+ if (! $this->canEncode($value)) {
+ throw UnsupportedValueException::invalidEncodableValue($value);
+ }
+
+ $result = $this->recursiveEncode($value->operator);
+
+ // Transform the result into an stdClass if a document is provided
+ if (! $value->operator instanceof WindowInterface) {
+ if (! is_first_key_operator($result)) {
+ $firstKey = array_key_first((array) $result);
+
+ throw new LogicException(sprintf('Expected OutputWindow::$operator to be an operator. Got "%s"', $firstKey ?? 'null'));
+ }
+
+ $result = (object) $result;
+ }
+
+ if (! $result instanceof stdClass) {
+ throw new LogicException(sprintf('Expected OutputWindow::$operator to be an stdClass, array or WindowInterface. Got "%s"', get_debug_type($result)));
+ }
+
+ if ($value->window !== Optional::Undefined) {
+ $result->window = $this->recursiveEncode($value->window);
+ }
+
+ return $result;
+ }
+}
diff --git a/src/Builder/Encoder/PipelineEncoder.php b/src/Builder/Encoder/PipelineEncoder.php
new file mode 100644
index 0000000..f0b319e
--- /dev/null
+++ b/src/Builder/Encoder/PipelineEncoder.php
@@ -0,0 +1,37 @@
+, Pipeline> */
+class PipelineEncoder extends AbstractExpressionEncoder
+{
+ /** @template-use EncodeIfSupported, Pipeline> */
+ use EncodeIfSupported;
+
+ /** @psalm-assert-if-true Pipeline $value */
+ public function canEncode(mixed $value): bool
+ {
+ return $value instanceof Pipeline;
+ }
+
+ /** @return list */
+ public function encode(mixed $value): array
+ {
+ if (! $this->canEncode($value)) {
+ throw UnsupportedValueException::invalidEncodableValue($value);
+ }
+
+ $encoded = [];
+ foreach ($value->getIterator() as $stage) {
+ $encoded[] = $this->encoder->encodeIfSupported($stage);
+ }
+
+ return $encoded;
+ }
+}
diff --git a/src/Builder/Encoder/QueryEncoder.php b/src/Builder/Encoder/QueryEncoder.php
new file mode 100644
index 0000000..d589050
--- /dev/null
+++ b/src/Builder/Encoder/QueryEncoder.php
@@ -0,0 +1,57 @@
+ */
+class QueryEncoder extends AbstractExpressionEncoder
+{
+ /** @template-use EncodeIfSupported */
+ use EncodeIfSupported;
+
+ public function canEncode(mixed $value): bool
+ {
+ return $value instanceof QueryObject;
+ }
+
+ public function encode(mixed $value): stdClass
+ {
+ if (! $this->canEncode($value)) {
+ throw UnsupportedValueException::invalidEncodableValue($value);
+ }
+
+ $result = new stdClass();
+ foreach ($value->queries as $key => $value) {
+ if ($value instanceof QueryInterface) {
+ // The sub-objects is merged into the main object, replacing duplicate keys
+ foreach (get_object_vars($this->recursiveEncode($value)) as $subKey => $subValue) {
+ if (property_exists($result, $subKey)) {
+ throw new LogicException(sprintf('Duplicate key "%s" in query object', $subKey));
+ }
+
+ $result->{$subKey} = $subValue;
+ }
+ } else {
+ if (property_exists($result, (string) $key)) {
+ throw new LogicException(sprintf('Duplicate key "%s" in query object', $key));
+ }
+
+ $result->{$key} = $this->encoder->encodeIfSupported($value);
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/src/Builder/Encoder/VariableEncoder.php b/src/Builder/Encoder/VariableEncoder.php
new file mode 100644
index 0000000..726acb9
--- /dev/null
+++ b/src/Builder/Encoder/VariableEncoder.php
@@ -0,0 +1,31 @@
+ */
+class VariableEncoder extends AbstractExpressionEncoder
+{
+ /** @template-use EncodeIfSupported */
+ use EncodeIfSupported;
+
+ public function canEncode(mixed $value): bool
+ {
+ return $value instanceof Variable;
+ }
+
+ public function encode(mixed $value): string
+ {
+ if (! $this->canEncode($value)) {
+ throw UnsupportedValueException::invalidEncodableValue($value);
+ }
+
+ // TODO: needs method because interfaces can't have properties
+ return '$$' . $value->name;
+ }
+}
diff --git a/src/Builder/Type/CombinedFieldQuery.php b/src/Builder/Type/CombinedFieldQuery.php
index b85096e..7619d0c 100644
--- a/src/Builder/Type/CombinedFieldQuery.php
+++ b/src/Builder/Type/CombinedFieldQuery.php
@@ -37,15 +37,24 @@ public function __construct(array $fieldQueries)
}
// Flatten nested CombinedFieldQuery
- $this->fieldQueries = array_reduce($fieldQueries, static function (array $fieldQueries, QueryInterface|FieldQueryInterface|Type|stdClass|array|bool|float|int|string|null $fieldQuery): array {
- if ($fieldQuery instanceof CombinedFieldQuery) {
- return array_merge($fieldQueries, $fieldQuery->fieldQueries);
- }
+ $this->fieldQueries = array_reduce(
+ $fieldQueries,
+ /**
+ * @param list $fieldQueries
+ *
+ * @return list
+ */
+ static function (array $fieldQueries, QueryInterface|FieldQueryInterface|Type|stdClass|array|bool|float|int|string|null $fieldQuery): array {
+ if ($fieldQuery instanceof CombinedFieldQuery) {
+ return array_merge($fieldQueries, $fieldQuery->fieldQueries);
+ }
- $fieldQueries[] = $fieldQuery;
+ $fieldQueries[] = $fieldQuery;
- return $fieldQueries;
- }, []);
+ return $fieldQueries;
+ },
+ [],
+ );
// Validate FieldQuery types and non-duplicate operators
$seenOperators = [];
diff --git a/src/Builder/Type/Encode.php b/src/Builder/Type/Encode.php
index d5ec603..c42b158 100644
--- a/src/Builder/Type/Encode.php
+++ b/src/Builder/Type/Encode.php
@@ -37,4 +37,9 @@ enum Encode
* Specific for $group stage
*/
case Group;
+
+ /**
+ * Default case used in the interface; implementing classes are expected to override this value
+ */
+ case Undefined;
}
diff --git a/src/Builder/Type/OperatorInterface.php b/src/Builder/Type/OperatorInterface.php
index 6622dbf..f219fb5 100644
--- a/src/Builder/Type/OperatorInterface.php
+++ b/src/Builder/Type/OperatorInterface.php
@@ -9,5 +9,8 @@
*/
interface OperatorInterface
{
+ /** To be overridden by implementing classes */
+ public const ENCODE = Encode::Undefined;
+
public function getOperator(): string;
}
diff --git a/src/Builder/Type/QueryObject.php b/src/Builder/Type/QueryObject.php
index 293350b..e07877f 100644
--- a/src/Builder/Type/QueryObject.php
+++ b/src/Builder/Type/QueryObject.php
@@ -4,9 +4,7 @@
namespace MongoDB\Builder\Type;
-use MongoDB\BSON\Decimal128;
-use MongoDB\BSON\Int64;
-use MongoDB\BSON\Regex;
+use MongoDB\BSON\Type;
use MongoDB\Exception\InvalidArgumentException;
use stdClass;
@@ -27,7 +25,7 @@ final class QueryObject implements QueryInterface
{
public readonly array $queries;
- /** @param array $queries */
+ /** @param array $queries */
public static function create(array $queries): QueryInterface
{
// We don't wrap a single query in a QueryObject
@@ -38,7 +36,7 @@ public static function create(array $queries): QueryInterface
return new self($queries);
}
- /** @param array $queriesOrArrayOfQueries */
+ /** @param array $queriesOrArrayOfQueries */
private function __construct(array $queriesOrArrayOfQueries)
{
// If the first element is an array and not an operator, we assume variadic arguments were not used