diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index fb84ad5ca..a02fc0fc8 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -41,12 +41,10 @@
-
-
-
-
-
-
+
+
+ encode($value)]]>
+
@@ -99,6 +97,13 @@
+
+
+
+
+
+
+
diff --git a/src/Builder/BuilderEncoder.php b/src/Builder/BuilderEncoder.php
index a630d4f0d..695d3fb14 100644
--- a/src/Builder/BuilderEncoder.php
+++ b/src/Builder/BuilderEncoder.php
@@ -9,7 +9,6 @@
use MongoDB\Builder\Encoder\CombinedFieldQueryEncoder;
use MongoDB\Builder\Encoder\DateTimeEncoder;
use MongoDB\Builder\Encoder\DictionaryEncoder;
-use MongoDB\Builder\Encoder\ExpressionEncoder;
use MongoDB\Builder\Encoder\FieldPathEncoder;
use MongoDB\Builder\Encoder\OperatorEncoder;
use MongoDB\Builder\Encoder\OutputWindowEncoder;
@@ -33,6 +32,7 @@
use function array_key_exists;
use function is_object;
+use function is_string;
/** @template-implements Encoder */
final class BuilderEncoder implements Encoder
@@ -40,7 +40,7 @@ final class BuilderEncoder implements Encoder
/** @template-use EncodeIfSupported */
use EncodeIfSupported;
- /** @var array> */
+ /** @var array> */
private array $defaultEncoders = [
Pipeline::class => PipelineEncoder::class,
Variable::class => VariableEncoder::class,
@@ -53,10 +53,10 @@ final class BuilderEncoder implements Encoder
DateTimeInterface::class => DateTimeEncoder::class,
];
- /** @var array */
+ /** @var array */
private array $cachedEncoders = [];
- /** @param array> $customEncoders */
+ /** @param array $customEncoders */
public function __construct(private readonly array $customEncoders = [])
{
}
@@ -82,7 +82,7 @@ public function encode(mixed $value): Type|stdClass|array|string|int
return $encoder->encode($value);
}
- private function getEncoderFor(object $value): ExpressionEncoder|null
+ private function getEncoderFor(object $value): Encoder|null
{
$valueClass = $value::class;
if (array_key_exists($valueClass, $this->cachedEncoders)) {
@@ -93,13 +93,22 @@ private function getEncoderFor(object $value): ExpressionEncoder|null
// First attempt: match class name exactly
if (isset($encoderList[$valueClass])) {
- return $this->cachedEncoders[$valueClass] = new $encoderList[$valueClass]($this);
+ $encoder = $encoderList[$valueClass];
+ if (is_string($encoder)) {
+ $encoder = new $encoder($this);
+ }
+
+ return $this->cachedEncoders[$valueClass] = $encoder;
}
// Second attempt: catch child classes
- foreach ($encoderList as $className => $encoderClass) {
+ foreach ($encoderList as $className => $encoder) {
if ($value instanceof $className) {
- return $this->cachedEncoders[$valueClass] = new $encoderClass($this);
+ if (is_string($encoder)) {
+ $encoder = new $encoder($this);
+ }
+
+ return $this->cachedEncoders[$valueClass] = $encoder;
}
}
diff --git a/src/Builder/Encoder/CombinedFieldQueryEncoder.php b/src/Builder/Encoder/CombinedFieldQueryEncoder.php
index b933953be..118757901 100644
--- a/src/Builder/Encoder/CombinedFieldQueryEncoder.php
+++ b/src/Builder/Encoder/CombinedFieldQueryEncoder.php
@@ -7,6 +7,7 @@
use LogicException;
use MongoDB\Builder\Type\CombinedFieldQuery;
use MongoDB\Codec\EncodeIfSupported;
+use MongoDB\Codec\Encoder;
use MongoDB\Exception\UnsupportedValueException;
use stdClass;
@@ -17,13 +18,14 @@
use function sprintf;
/**
- * @template-extends AbstractExpressionEncoder
+ * @template-implements Encoder
* @internal
*/
-final class CombinedFieldQueryEncoder extends AbstractExpressionEncoder
+final class CombinedFieldQueryEncoder implements Encoder
{
/** @template-use EncodeIfSupported */
use EncodeIfSupported;
+ use RecursiveEncode;
public function canEncode(mixed $value): bool
{
diff --git a/src/Builder/Encoder/DateTimeEncoder.php b/src/Builder/Encoder/DateTimeEncoder.php
index c4b98f2c4..6926bd813 100644
--- a/src/Builder/Encoder/DateTimeEncoder.php
+++ b/src/Builder/Encoder/DateTimeEncoder.php
@@ -7,13 +7,14 @@
use DateTimeInterface;
use MongoDB\BSON\UTCDateTime;
use MongoDB\Codec\EncodeIfSupported;
+use MongoDB\Codec\Encoder;
use MongoDB\Exception\UnsupportedValueException;
/**
- * @template-extends AbstractExpressionEncoder
+ * @template-implements Encoder
* @internal
*/
-final class DateTimeEncoder extends AbstractExpressionEncoder
+final class DateTimeEncoder implements Encoder
{
/** @template-use EncodeIfSupported */
use EncodeIfSupported;
diff --git a/src/Builder/Encoder/DictionaryEncoder.php b/src/Builder/Encoder/DictionaryEncoder.php
index f228fa2d7..dc89cff11 100644
--- a/src/Builder/Encoder/DictionaryEncoder.php
+++ b/src/Builder/Encoder/DictionaryEncoder.php
@@ -6,14 +6,15 @@
use MongoDB\Builder\Type\DictionaryInterface;
use MongoDB\Codec\EncodeIfSupported;
+use MongoDB\Codec\Encoder;
use MongoDB\Exception\UnsupportedValueException;
use stdClass;
/**
- * @template-extends AbstractExpressionEncoder
+ * @template-implements Encoder
* @internal
*/
-final class DictionaryEncoder extends AbstractExpressionEncoder
+final class DictionaryEncoder implements Encoder
{
/** @template-use EncodeIfSupported */
use EncodeIfSupported;
diff --git a/src/Builder/Encoder/ExpressionEncoder.php b/src/Builder/Encoder/ExpressionEncoder.php
deleted file mode 100644
index f50da33d3..000000000
--- a/src/Builder/Encoder/ExpressionEncoder.php
+++ /dev/null
@@ -1,21 +0,0 @@
-
- * @internal
- */
-interface ExpressionEncoder extends Encoder
-{
- public function __construct(BuilderEncoder $encoder);
-}
diff --git a/src/Builder/Encoder/FieldPathEncoder.php b/src/Builder/Encoder/FieldPathEncoder.php
index 4dc7b6f2c..3766f9a0e 100644
--- a/src/Builder/Encoder/FieldPathEncoder.php
+++ b/src/Builder/Encoder/FieldPathEncoder.php
@@ -6,13 +6,14 @@
use MongoDB\Builder\Type\FieldPathInterface;
use MongoDB\Codec\EncodeIfSupported;
+use MongoDB\Codec\Encoder;
use MongoDB\Exception\UnsupportedValueException;
/**
- * @template-extends AbstractExpressionEncoder
+ * @template-implements Encoder
* @internal
*/
-final class FieldPathEncoder extends AbstractExpressionEncoder
+final class FieldPathEncoder implements Encoder
{
/** @template-use EncodeIfSupported */
use EncodeIfSupported;
diff --git a/src/Builder/Encoder/OperatorEncoder.php b/src/Builder/Encoder/OperatorEncoder.php
index 5c18d7c0b..8f81edfa2 100644
--- a/src/Builder/Encoder/OperatorEncoder.php
+++ b/src/Builder/Encoder/OperatorEncoder.php
@@ -9,6 +9,7 @@
use MongoDB\Builder\Type\OperatorInterface;
use MongoDB\Builder\Type\Optional;
use MongoDB\Codec\EncodeIfSupported;
+use MongoDB\Codec\Encoder;
use MongoDB\Exception\UnsupportedValueException;
use stdClass;
@@ -17,13 +18,14 @@
use function sprintf;
/**
- * @template-extends AbstractExpressionEncoder
+ * @template-implements Encoder
* @internal
*/
-final class OperatorEncoder extends AbstractExpressionEncoder
+final class OperatorEncoder implements Encoder
{
/** @template-use EncodeIfSupported */
use EncodeIfSupported;
+ use RecursiveEncode;
public function canEncode(mixed $value): bool
{
diff --git a/src/Builder/Encoder/OutputWindowEncoder.php b/src/Builder/Encoder/OutputWindowEncoder.php
index 830c2cacb..4e435a987 100644
--- a/src/Builder/Encoder/OutputWindowEncoder.php
+++ b/src/Builder/Encoder/OutputWindowEncoder.php
@@ -9,6 +9,7 @@
use MongoDB\Builder\Type\OutputWindow;
use MongoDB\Builder\Type\WindowInterface;
use MongoDB\Codec\EncodeIfSupported;
+use MongoDB\Codec\Encoder;
use MongoDB\Exception\UnsupportedValueException;
use stdClass;
@@ -18,13 +19,14 @@
use function sprintf;
/**
- * @template-extends AbstractExpressionEncoder
+ * @template-implements Encoder
* @internal
*/
-final class OutputWindowEncoder extends AbstractExpressionEncoder
+final class OutputWindowEncoder implements Encoder
{
/** @template-use EncodeIfSupported */
use EncodeIfSupported;
+ use RecursiveEncode;
public function canEncode(mixed $value): bool
{
diff --git a/src/Builder/Encoder/PipelineEncoder.php b/src/Builder/Encoder/PipelineEncoder.php
index 5a343e0fb..c4c0829ce 100644
--- a/src/Builder/Encoder/PipelineEncoder.php
+++ b/src/Builder/Encoder/PipelineEncoder.php
@@ -6,16 +6,18 @@
use MongoDB\Builder\Pipeline;
use MongoDB\Codec\EncodeIfSupported;
+use MongoDB\Codec\Encoder;
use MongoDB\Exception\UnsupportedValueException;
/**
- * @template-extends AbstractExpressionEncoder, Pipeline>
+ * @template-implements Encoder, Pipeline>
* @internal
*/
-final class PipelineEncoder extends AbstractExpressionEncoder
+final class PipelineEncoder implements Encoder
{
/** @template-use EncodeIfSupported, Pipeline> */
use EncodeIfSupported;
+ use RecursiveEncode;
/** @psalm-assert-if-true Pipeline $value */
public function canEncode(mixed $value): bool
@@ -32,7 +34,7 @@ public function encode(mixed $value): array
$encoded = [];
foreach ($value->getIterator() as $stage) {
- $encoded[] = $this->encoder->encodeIfSupported($stage);
+ $encoded[] = $this->recursiveEncode($stage);
}
return $encoded;
diff --git a/src/Builder/Encoder/QueryEncoder.php b/src/Builder/Encoder/QueryEncoder.php
index 2193edb02..6a43f37c0 100644
--- a/src/Builder/Encoder/QueryEncoder.php
+++ b/src/Builder/Encoder/QueryEncoder.php
@@ -8,6 +8,7 @@
use MongoDB\Builder\Type\QueryInterface;
use MongoDB\Builder\Type\QueryObject;
use MongoDB\Codec\EncodeIfSupported;
+use MongoDB\Codec\Encoder;
use MongoDB\Exception\UnsupportedValueException;
use stdClass;
@@ -16,13 +17,14 @@
use function sprintf;
/**
- * @template-extends AbstractExpressionEncoder
+ * @template-implements Encoder
* @internal
*/
-final class QueryEncoder extends AbstractExpressionEncoder
+final class QueryEncoder implements Encoder
{
/** @template-use EncodeIfSupported */
use EncodeIfSupported;
+ use RecursiveEncode;
public function canEncode(mixed $value): bool
{
diff --git a/src/Builder/Encoder/AbstractExpressionEncoder.php b/src/Builder/Encoder/RecursiveEncode.php
similarity index 75%
rename from src/Builder/Encoder/AbstractExpressionEncoder.php
rename to src/Builder/Encoder/RecursiveEncode.php
index 7a0f437b2..d93fdd998 100644
--- a/src/Builder/Encoder/AbstractExpressionEncoder.php
+++ b/src/Builder/Encoder/RecursiveEncode.php
@@ -4,20 +4,13 @@
namespace MongoDB\Builder\Encoder;
-use MongoDB\BSON\Type;
use MongoDB\Builder\BuilderEncoder;
use stdClass;
use function get_object_vars;
use function is_array;
-/**
- * @template BSONType of Type|stdClass|array|string|int
- * @template NativeType
- * @template-implements ExpressionEncoder
- * @internal
- */
-abstract class AbstractExpressionEncoder implements ExpressionEncoder
+trait RecursiveEncode
{
final public function __construct(protected readonly BuilderEncoder $encoder)
{
@@ -33,7 +26,7 @@ final public function __construct(protected readonly BuilderEncoder $encoder)
*
* @template T
*/
- final protected function recursiveEncode(mixed $value): mixed
+ private function recursiveEncode(mixed $value): mixed
{
if (is_array($value)) {
foreach ($value as $key => $val) {
diff --git a/src/Builder/Encoder/VariableEncoder.php b/src/Builder/Encoder/VariableEncoder.php
index 31f73b6d8..051977976 100644
--- a/src/Builder/Encoder/VariableEncoder.php
+++ b/src/Builder/Encoder/VariableEncoder.php
@@ -6,13 +6,14 @@
use MongoDB\Builder\Expression\Variable;
use MongoDB\Codec\EncodeIfSupported;
+use MongoDB\Codec\Encoder;
use MongoDB\Exception\UnsupportedValueException;
/**
- * @template-extends AbstractExpressionEncoder
+ * @template-implements Encoder
* @internal
*/
-final class VariableEncoder extends AbstractExpressionEncoder
+final class VariableEncoder implements Encoder
{
/** @template-use EncodeIfSupported */
use EncodeIfSupported;
diff --git a/tests/Builder/BuilderEncoderTest.php b/tests/Builder/BuilderEncoderTest.php
index 893803a94..719ab9f7e 100644
--- a/tests/Builder/BuilderEncoderTest.php
+++ b/tests/Builder/BuilderEncoderTest.php
@@ -15,8 +15,11 @@
use MongoDB\Builder\Pipeline;
use MongoDB\Builder\Query;
use MongoDB\Builder\Stage;
+use MongoDB\Builder\Type\FieldPathInterface;
use MongoDB\Builder\Type\Sort;
use MongoDB\Builder\Variable;
+use MongoDB\Codec\EncodeIfSupported;
+use MongoDB\Codec\Encoder;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
@@ -379,10 +382,50 @@ public function testDateTimeEncoding(): void
$this->assertSamePipeline($expected, $pipeline);
}
+ public function testCustomEncoder(): void
+ {
+ $customEncoders = [
+ FieldPathInterface::class => new class implements Encoder {
+ use EncodeIfSupported;
+
+ public function canEncode(mixed $value): bool
+ {
+ return $value instanceof FieldPathInterface;
+ }
+
+ public function encode(mixed $value)
+ {
+ return '$prefix.' . $value->name;
+ }
+ },
+ ];
+ $codec = new BuilderEncoder($customEncoders);
+
+ $pipeline = new Pipeline(
+ Stage::project(
+ threeFavorites: Expression::slice(
+ Expression::arrayFieldPath('items'),
+ n: 3,
+ ),
+ ),
+ );
+
+ $expected = [
+ [
+ '$project' => [
+ 'threeFavorites' => [
+ '$slice' => ['$prefix.items', 3],
+ ],
+ ],
+ ],
+ ];
+
+ $this->assertSamePipeline($expected, $pipeline, $codec);
+ }
+
/** @param list> $expected */
- private static function assertSamePipeline(array $expected, Pipeline $pipeline): void
+ private static function assertSamePipeline(array $expected, Pipeline $pipeline, $codec = new BuilderEncoder()): void
{
- $codec = new BuilderEncoder();
$actual = $codec->encode($pipeline);
// Normalize with BSON round-trip