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