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