diff --git a/src/Denormalizer/Denormalizer.php b/src/Denormalizer/Denormalizer.php index 3e64fdd..e5c80cd 100644 --- a/src/Denormalizer/Denormalizer.php +++ b/src/Denormalizer/Denormalizer.php @@ -9,6 +9,7 @@ use Chubbyphp\Deserialization\DeserializerRuntimeException; use Chubbyphp\Deserialization\Mapping\DenormalizationFieldMappingInterface; use Chubbyphp\Deserialization\Mapping\DenormalizationObjectMappingInterface; +use Chubbyphp\Deserialization\Policy\GroupPolicy; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; @@ -157,6 +158,10 @@ private function denormalizeField( array $data, $object ) { + if (!$this->isCompliant($context, $denormalizationFieldMapping, $object)) { + return; + } + if (!$this->isWithinGroup($context, $denormalizationFieldMapping)) { return; } @@ -184,6 +189,25 @@ private function handleNotAllowedAdditionalFields(string $path, array $names) throw $exception; } + /** + * @param DenormalizerContextInterface $context + * @param DenormalizationFieldMappingInterface $mapping + * @param object $object + * + * @return bool + */ + private function isCompliant( + DenormalizerContextInterface $context, + DenormalizationFieldMappingInterface $mapping, + $object + ): bool { + if (!is_callable([$mapping, 'getPolicy'])) { + return true; + } + + return $mapping->getPolicy()->isCompliant($context, $object); + } + /** * @param DenormalizerContextInterface $context * @param DenormalizationFieldMappingInterface $fieldMapping @@ -198,6 +222,15 @@ private function isWithinGroup( return true; } + @trigger_error( + sprintf( + 'Use "%s" instead of "%s::setGroups"', + GroupPolicy::class, + DenormalizerContextInterface::class + ), + E_USER_DEPRECATED + ); + foreach ($fieldMapping->getGroups() as $group) { if (in_array($group, $groups, true)) { return true; diff --git a/src/Denormalizer/DenormalizerContext.php b/src/Denormalizer/DenormalizerContext.php index 4da5fa7..10724f4 100644 --- a/src/Denormalizer/DenormalizerContext.php +++ b/src/Denormalizer/DenormalizerContext.php @@ -14,6 +14,8 @@ final class DenormalizerContext implements DenormalizerContextInterface private $allowedAdditionalFields; /** + * @deprecated + * * @var string[] */ private $groups = []; @@ -28,17 +30,24 @@ final class DenormalizerContext implements DenormalizerContextInterface */ private $resetMissingFields; + /** + * @var array + */ + private $attributes; + /** * @param array|null $allowedAdditionalFields * @param string[] $groups * @param ServerRequestInterface|null $request * @param bool $resetMissingFields + * @param array $attributes */ public function __construct( array $allowedAdditionalFields = null, array $groups = [], ServerRequestInterface $request = null, - bool $resetMissingFields = false + bool $resetMissingFields = false, + array $attributes = [] ) { $this->allowedAdditionalFields = $allowedAdditionalFields; $this->groups = $groups; @@ -52,6 +61,7 @@ public function __construct( } $this->resetMissingFields = $resetMissingFields; + $this->attributes = $attributes; } /** @@ -63,6 +73,8 @@ public function getAllowedAdditionalFields() } /** + * @deprecated + * * @return string[] */ public function getGroups(): array @@ -87,4 +99,41 @@ public function isResetMissingFields(): bool { return $this->resetMissingFields; } + + /** + * @return array + */ + public function getAttributes(): array + { + return $this->attributes; + } + + /** + * @param string $name + * @param mixed $default + * + * @return mixed + */ + public function getAttribute(string $name, $default = null) + { + if (isset($this->attributes[$name])) { + return $this->attributes[$name]; + } + + return $default; + } + + /** + * @param string $name + * @param mixed $value + * + * @return DenormalizerContextInterface + */ + public function withAttribute(string $name, $value): DenormalizerContextInterface + { + $context = clone $this; + $context->attributes[$name] = $value; + + return $context; + } } diff --git a/src/Denormalizer/DenormalizerContextBuilder.php b/src/Denormalizer/DenormalizerContextBuilder.php index fea3a6f..0a745d2 100644 --- a/src/Denormalizer/DenormalizerContextBuilder.php +++ b/src/Denormalizer/DenormalizerContextBuilder.php @@ -14,6 +14,8 @@ final class DenormalizerContextBuilder implements DenormalizerContextBuilderInte private $allowedAdditionalFields; /** + * @deprecated + * * @var string[] */ private $groups = []; @@ -28,6 +30,11 @@ final class DenormalizerContextBuilder implements DenormalizerContextBuilderInte */ private $resetMissingFields = false; + /** + * @var array + */ + private $attributes = []; + private function __construct() { } @@ -54,6 +61,8 @@ public function setAllowedAdditionalFields( } /** + * @deprecated + * * @param string[] $groups * * @return DenormalizerContextBuilderInterface @@ -96,6 +105,18 @@ public function setResetMissingFields(bool $resetMissingFields): DenormalizerCon return $this; } + /** + * @param array $attributes + * + * @return DenormalizerContextBuilderInterface + */ + public function setAttributes(array $attributes): DenormalizerContextBuilderInterface + { + $this->attributes = $attributes; + + return $this; + } + /** * @return DenormalizerContextInterface */ @@ -105,7 +126,8 @@ public function getContext(): DenormalizerContextInterface $this->allowedAdditionalFields, $this->groups, $this->request, - $this->resetMissingFields + $this->resetMissingFields, + $this->attributes ); } } diff --git a/src/Denormalizer/DenormalizerContextBuilderInterface.php b/src/Denormalizer/DenormalizerContextBuilderInterface.php index e38105a..c8f4ffa 100644 --- a/src/Denormalizer/DenormalizerContextBuilderInterface.php +++ b/src/Denormalizer/DenormalizerContextBuilderInterface.php @@ -6,6 +6,9 @@ use Psr\Http\Message\ServerRequestInterface; +/** + * @method setAttributes(array $attributes): self + */ interface DenormalizerContextBuilderInterface { /** @@ -21,6 +24,8 @@ public static function create(): self; public function setAllowedAdditionalFields(array $allowedAdditionalFields = null): self; /** + * @deprecated + * * @param string[] $groups * * @return self @@ -34,12 +39,12 @@ public function setGroups(array $groups): self; */ public function setRequest(ServerRequestInterface $request = null): self; - // /** - // * @param bool $resetMissingFields - // * - // * @return self - // */ - // public function setResetMissingFields(bool $resetMissingFields): self; + /** + * @param array $attributes + * + * @return self + */ + //public function setAttributes(array $attributes): self; /** * @return DenormalizerContextInterface diff --git a/src/Denormalizer/DenormalizerContextInterface.php b/src/Denormalizer/DenormalizerContextInterface.php index 6870b1f..1904abe 100644 --- a/src/Denormalizer/DenormalizerContextInterface.php +++ b/src/Denormalizer/DenormalizerContextInterface.php @@ -6,6 +6,11 @@ use Psr\Http\Message\ServerRequestInterface; +/** + * @method getAttributes(): array + * @method getAttribute(string $name, $default = null) + * @method withAttribute(string $name, $value): self + */ interface DenormalizerContextInterface { /** @@ -14,6 +19,8 @@ interface DenormalizerContextInterface public function getAllowedAdditionalFields(); /** + * @deprecated + * * @return string[] */ public function getGroups(): array; @@ -23,8 +30,23 @@ public function getGroups(): array; */ public function getRequest(); - // /** - // * @return bool - // */ - // public function isResetMissingFields(): bool; + /* + * @return array + */ + //public function getAttributes(): array; + + /* + * @param string $name + * @param mixed $default + * + * @return mixed + */ + //public function getAttribute(string $name, $default = null); + + /* + * @param string $name + * @param mixed $value + * @return self + */ + //public function withAttribute(string $name, $value): self; } diff --git a/src/Mapping/DenormalizationFieldMapping.php b/src/Mapping/DenormalizationFieldMapping.php index ae92212..66704d2 100644 --- a/src/Mapping/DenormalizationFieldMapping.php +++ b/src/Mapping/DenormalizationFieldMapping.php @@ -5,6 +5,8 @@ namespace Chubbyphp\Deserialization\Mapping; use Chubbyphp\Deserialization\Denormalizer\FieldDenormalizerInterface; +use Chubbyphp\Deserialization\Policy\NullPolicy; +use Chubbyphp\Deserialization\Policy\PolicyInterface; final class DenormalizationFieldMapping implements DenormalizationFieldMappingInterface { @@ -14,6 +16,8 @@ final class DenormalizationFieldMapping implements DenormalizationFieldMappingIn private $name; /** + * @deprecated + * * @var array */ private $groups; @@ -23,16 +27,27 @@ final class DenormalizationFieldMapping implements DenormalizationFieldMappingIn */ private $fieldDenormalizer; + /** + * @var PolicyInterface + */ + private $policy; + /** * @param string $name * @param array $groups * @param FieldDenormalizerInterface $fieldDenormalizer + * @param PolicyInterface|null $policy */ - public function __construct($name, array $groups = [], FieldDenormalizerInterface $fieldDenormalizer) - { + public function __construct( + $name, + array $groups = [], + FieldDenormalizerInterface $fieldDenormalizer, + PolicyInterface $policy = null + ) { $this->name = $name; $this->groups = $groups; $this->fieldDenormalizer = $fieldDenormalizer; + $this->policy = $policy ?? new NullPolicy(); } /** @@ -44,6 +59,8 @@ public function getName(): string } /** + * @deprecated + * * @return array */ public function getGroups(): array @@ -58,4 +75,12 @@ public function getFieldDenormalizer(): FieldDenormalizerInterface { return $this->fieldDenormalizer; } + + /** + * @return PolicyInterface + */ + public function getPolicy(): PolicyInterface + { + return $this->policy; + } } diff --git a/src/Mapping/DenormalizationFieldMappingBuilder.php b/src/Mapping/DenormalizationFieldMappingBuilder.php index 3a12c04..ed954ae 100644 --- a/src/Mapping/DenormalizationFieldMappingBuilder.php +++ b/src/Mapping/DenormalizationFieldMappingBuilder.php @@ -14,6 +14,8 @@ use Chubbyphp\Deserialization\Denormalizer\Relation\EmbedOneFieldDenormalizer; use Chubbyphp\Deserialization\Denormalizer\Relation\ReferenceManyFieldDenormalizer; use Chubbyphp\Deserialization\Denormalizer\Relation\ReferenceOneFieldDenormalizer; +use Chubbyphp\Deserialization\Policy\NullPolicy; +use Chubbyphp\Deserialization\Policy\PolicyInterface; final class DenormalizationFieldMappingBuilder implements DenormalizationFieldMappingBuilderInterface { @@ -23,6 +25,8 @@ final class DenormalizationFieldMappingBuilder implements DenormalizationFieldMa private $name; /** + * @deprecated + * * @var array */ private $groups = []; @@ -32,6 +36,14 @@ final class DenormalizationFieldMappingBuilder implements DenormalizationFieldMa */ private $fieldDenormalizer; + /** + * @var PolicyInterface|null + */ + private $policy; + + /** + * @param string $name + */ private function __construct(string $name) { $this->name = $name; @@ -39,13 +51,22 @@ private function __construct(string $name) /** * @param string $name + * @param bool $emptyToNull + * @param FieldDenormalizerInterface|null * * @return DenormalizationFieldMappingBuilderInterface */ - public static function create(string $name, bool $emptyToNull = false): DenormalizationFieldMappingBuilderInterface - { + public static function create( + string $name, + bool $emptyToNull = false, + FieldDenormalizerInterface $fieldDenormalizer = null + ): DenormalizationFieldMappingBuilderInterface { + if (null === $fieldDenormalizer) { + $fieldDenormalizer = new FieldDenormalizer(new PropertyAccessor($name), $emptyToNull); + } + $self = new self($name); - $self->fieldDenormalizer = new FieldDenormalizer(new PropertyAccessor($name), $emptyToNull); + $self->fieldDenormalizer = $fieldDenormalizer; return $self; } @@ -167,6 +188,8 @@ public static function createReferenceOne( } /** + * @deprecated + * * @param array $groups * * @return DenormalizationFieldMappingBuilderInterface @@ -186,11 +209,28 @@ public function setGroups(array $groups): DenormalizationFieldMappingBuilderInte public function setFieldDenormalizer( FieldDenormalizerInterface $fieldDenormalizer ): DenormalizationFieldMappingBuilderInterface { + @trigger_error( + 'Utilize third parameter of create method instead', + E_USER_DEPRECATED + ); + $this->fieldDenormalizer = $fieldDenormalizer; return $this; } + /** + * @param PolicyInterface $policy + * + * @return DenormalizationFieldMappingBuilderInterface + */ + public function setPolicy(PolicyInterface $policy): DenormalizationFieldMappingBuilderInterface + { + $this->policy = $policy; + + return $this; + } + /** * @return DenormalizationFieldMappingInterface */ @@ -199,7 +239,8 @@ public function getMapping(): DenormalizationFieldMappingInterface return new DenormalizationFieldMapping( $this->name, $this->groups, - $this->fieldDenormalizer + $this->fieldDenormalizer, + $this->policy ?? new NullPolicy() ); } } diff --git a/src/Mapping/DenormalizationFieldMappingBuilderInterface.php b/src/Mapping/DenormalizationFieldMappingBuilderInterface.php index 83bf5db..31a331d 100644 --- a/src/Mapping/DenormalizationFieldMappingBuilderInterface.php +++ b/src/Mapping/DenormalizationFieldMappingBuilderInterface.php @@ -5,7 +5,11 @@ namespace Chubbyphp\Deserialization\Mapping; use Chubbyphp\Deserialization\Denormalizer\FieldDenormalizerInterface; +use Chubbyphp\Deserialization\Policy\PolicyInterface; +/** + * @method setPolicy(PolicyInterface $policy): self + */ interface DenormalizationFieldMappingBuilderInterface { /** @@ -16,6 +20,16 @@ interface DenormalizationFieldMappingBuilderInterface public static function create(string $name): self; /** + * @param string $name + * @param FieldNormalizerInterface|null $fieldNormalizer + * + * @return NormalizationFieldMappingBuilderInterface + */ + //public static function create(string $name, FieldNormalizerInterface $fieldNormalizer = null): self; + + /** + * @deprecated + * * @param array $groups * * @return self @@ -29,6 +43,13 @@ public function setGroups(array $groups): self; */ public function setFieldDenormalizer(FieldDenormalizerInterface $fieldDenormalizer): self; + /** + * @param PolicyInterface $policy + * + * @return self + */ + //public function setPolicy(PolicyInterface $policy): self; + /** * @return DenormalizationFieldMappingInterface */ diff --git a/src/Mapping/DenormalizationFieldMappingInterface.php b/src/Mapping/DenormalizationFieldMappingInterface.php index 5042dc8..1cc50f6 100644 --- a/src/Mapping/DenormalizationFieldMappingInterface.php +++ b/src/Mapping/DenormalizationFieldMappingInterface.php @@ -5,7 +5,11 @@ namespace Chubbyphp\Deserialization\Mapping; use Chubbyphp\Deserialization\Denormalizer\FieldDenormalizerInterface; +use Chubbyphp\Deserialization\Policy\PolicyInterface; +/** + * @method getPolicy(): PolicyInterface + */ interface DenormalizationFieldMappingInterface { /** @@ -14,6 +18,8 @@ interface DenormalizationFieldMappingInterface public function getName(): string; /** + * @deprecated + * * @return array */ public function getGroups(): array; @@ -22,4 +28,9 @@ public function getGroups(): array; * @return FieldDenormalizerInterface */ public function getFieldDenormalizer(): FieldDenormalizerInterface; + + /* + * @return PolicyInterface + */ + //public function getPolicy(): PolicyInterface; } diff --git a/src/Policy/AndPolicy.php b/src/Policy/AndPolicy.php new file mode 100644 index 0000000..9ef9f80 --- /dev/null +++ b/src/Policy/AndPolicy.php @@ -0,0 +1,40 @@ +policies = $policies; + } + + /** + * @param DenormalizerContextInterface $context + * @param object|mixed $object + * + * @return bool + */ + public function isCompliant(DenormalizerContextInterface $context, $object): bool + { + foreach ($this->policies as $policy) { + if (!$policy->isCompliant($context, $object)) { + return false; + } + } + + return true; + } +} diff --git a/src/Policy/CallbackPolicy.php b/src/Policy/CallbackPolicy.php new file mode 100644 index 0000000..b986045 --- /dev/null +++ b/src/Policy/CallbackPolicy.php @@ -0,0 +1,34 @@ +callback = $callback; + } + + /** + * @param DenormalizerContextInterface $context + * @param object|mixed $object + * + * @return bool + */ + public function isCompliant(DenormalizerContextInterface $context, $object): bool + { + return ($this->callback)($context, $object); + } +} diff --git a/src/Policy/GroupPolicy.php b/src/Policy/GroupPolicy.php new file mode 100644 index 0000000..9d2d3d7 --- /dev/null +++ b/src/Policy/GroupPolicy.php @@ -0,0 +1,56 @@ +groups = $groups; + } + + /** + * @param DenormalizerContextInterface $context + * @param object|mixed $object + * + * @return bool + */ + public function isCompliant(DenormalizerContextInterface $context, $object): bool + { + if ([] === $this->groups) { + return true; + } + + $contextGroups = $context->getAttribute(self::ATTRIBUTE_GROUPS, [self::GROUP_DEFAULT]); + + foreach ($this->groups as $group) { + if (in_array($group, $contextGroups, true)) { + return true; + } + } + + return false; + } +} diff --git a/src/Policy/NullPolicy.php b/src/Policy/NullPolicy.php new file mode 100644 index 0000000..190bc7c --- /dev/null +++ b/src/Policy/NullPolicy.php @@ -0,0 +1,21 @@ +policies = $policies; + } + + /** + * @param DenormalizerContextInterface $context + * @param object|mixed $object + * + * @return bool + */ + public function isCompliant(DenormalizerContextInterface $context, $object): bool + { + foreach ($this->policies as $policy) { + if ($policy->isCompliant($context, $object)) { + return true; + } + } + + return false; + } +} diff --git a/src/Policy/PolicyInterface.php b/src/Policy/PolicyInterface.php new file mode 100644 index 0000000..a8a4db1 --- /dev/null +++ b/src/Policy/PolicyInterface.php @@ -0,0 +1,18 @@ +getGroups()); self::assertNull($context->getRequest()); self::assertFalse($context->isResetMissingFields()); + self::assertSame([], $context->getAttributes()); } public function testCreateWithSetResetMissingField() @@ -58,6 +59,7 @@ public function testCreateWithOverridenSettings() ->setAllowedAdditionalFields(['allowed_field']) ->setGroups(['group1']) ->setRequest($request) + ->setAttributes(['attribute' => 'value']) ->getContext() ; @@ -67,6 +69,7 @@ public function testCreateWithOverridenSettings() self::assertSame(['group1'], $context->getGroups()); self::assertSame($request, $context->getRequest()); self::assertFalse($context->isResetMissingFields()); + self::assertSame(['attribute' => 'value'], $context->getAttributes()); } public function testCreateSetNullRequest() diff --git a/tests/Denormalizer/DenormalizerContextTest.php b/tests/Denormalizer/DenormalizerContextTest.php index ff9af53..5330a91 100644 --- a/tests/Denormalizer/DenormalizerContextTest.php +++ b/tests/Denormalizer/DenormalizerContextTest.php @@ -27,6 +27,9 @@ public function testCreate() self::assertSame([], $context->getGroups()); self::assertNull($context->getRequest()); self::assertFalse($context->isResetMissingFields()); + self::assertSame([], $context->getAttributes()); + self::assertNull($context->getAttribute('nonExistingAttribute')); + self::assertSame('default', $context->getAttribute('nonExistingAttribute', 'default')); } public function testCreateWithOverridenSettings() @@ -34,12 +37,14 @@ public function testCreateWithOverridenSettings() /** @var ServerRequestInterface|MockObject $request */ $request = $this->getMockByCalls(ServerRequestInterface::class); - $context = new DenormalizerContext(['allowed_field'], ['group1'], $request, false); + $context = new DenormalizerContext(['allowed_field'], ['group1'], $request, false, ['attribute' => 'value']); self::assertSame(['allowed_field'], $context->getAllowedAdditionalFields()); self::assertSame(['group1'], $context->getGroups()); self::assertSame($request, $context->getRequest()); self::assertFalse($context->isResetMissingFields()); + self::assertSame(['attribute' => 'value'], $context->getAttributes()); + self::assertSame('value', $context->getAttribute('attribute')); } public function testWithResetMissingFieldsExpectDeprecation() @@ -57,4 +62,19 @@ public function testWithResetMissingFieldsExpectDeprecation() self::assertSame(E_USER_DEPRECATED, $error['type']); self::assertSame('resetMissingFields is broken by design, please do this your self by model or repository', $error['message']); } + + public function testWithAttribute() + { + /** @var ServerRequestInterface|MockObject $request */ + $request = $this->getMockByCalls(ServerRequestInterface::class); + + $context = new DenormalizerContext(['allowed_field'], ['group1'], $request, false, ['attribute' => 'value']); + + $newContext = $context->withAttribute('otherAttribute', 'value2'); + + self::assertNotSame($context, $newContext); + + self::assertSame(['attribute' => 'value', 'otherAttribute' => 'value2'], $newContext->getAttributes()); + self::assertSame(['attribute' => 'value'], $context->getAttributes()); + } } diff --git a/tests/Denormalizer/DenormalizerTest.php b/tests/Denormalizer/DenormalizerTest.php index 78d9d5d..6e47af9 100644 --- a/tests/Denormalizer/DenormalizerTest.php +++ b/tests/Denormalizer/DenormalizerTest.php @@ -14,6 +14,7 @@ use Chubbyphp\Deserialization\DeserializerRuntimeException; use Chubbyphp\Deserialization\Mapping\DenormalizationFieldMappingInterface; use Chubbyphp\Deserialization\Mapping\DenormalizationObjectMappingInterface; +use Chubbyphp\Deserialization\Policy\PolicyInterface; use Chubbyphp\Mock\Argument\ArgumentInstanceOf; use Chubbyphp\Mock\Call; use Chubbyphp\Mock\MockByCallsTrait; @@ -622,4 +623,178 @@ public function testDenormalizeWithGroupsButNoGroupOnField() self::assertSame($object, $denormalizer->denormalize(\stdClass::class, ['name' => 'name'], $context)); } + + public function testDenormalizeWithCompliantPolicy() + { + $object = new \stdClass(); + + $factory = function () use ($object) { + return $object; + }; + + /** @var DenormalizerContextInterface|MockObject $context */ + $context = $this->getMockByCalls(DenormalizerContextInterface::class, [ + Call::create('getGroups')->with()->willReturn([]), + Call::create('getAllowedAdditionalFields')->with()->willReturn(null), + ]); + + /** @var FieldDenormalizerInterface|MockObject $nameFieldDenormalizer */ + $nameFieldDenormalizer = $this->getMockByCalls(FieldDenormalizerInterface::class, [ + Call::create('denormalizeField') + ->with('name', $object, 'name', $context, new ArgumentInstanceOf(DenormalizerInterface::class)), + ]); + + /** @var PolicyInterface|MockObject $policy */ + $policy = $this->getMockByCalls(PolicyInterface::class, [ + Call::create('isCompliant')->with($context, $object)->willReturn(true), + ]); + + $denormalizationNameFieldMapping = $this->getDenormalizationFieldMappingWithPolicy( + 'name', + $nameFieldDenormalizer, + $policy + ); + + /** @var DenormalizationFieldMappingInterface|MockObject $denormalizationValueFieldMapping */ + $denormalizationValueFieldMapping = $this->getMockByCalls(DenormalizationFieldMappingInterface::class, [ + Call::create('getName')->with()->willReturn('value'), + ]); + + /** @var DenormalizationObjectMappingInterface|MockObject $objectMapping */ + $objectMapping = $this->getMockByCalls(DenormalizationObjectMappingInterface::class, [ + Call::create('getDenormalizationFactory')->with('', null)->willReturn($factory), + Call::create('getDenormalizationFieldMappings')->with('', null)->willReturn([ + $denormalizationNameFieldMapping, + $denormalizationValueFieldMapping, + ]), + ]); + + /** @var DenormalizerObjectMappingRegistryInterface|MockObject $registry */ + $registry = $this->getMockByCalls(DenormalizerObjectMappingRegistryInterface::class, [ + Call::create('getObjectMapping')->with(\stdClass::class)->willReturn($objectMapping), + ]); + + /** @var LoggerInterface|MockObject $logger */ + $logger = $this->getMockByCalls(LoggerInterface::class, [ + Call::create('info')->with('deserialize: path {path}', ['path' => 'name']), + ]); + + $denormalizer = new Denormalizer($registry, $logger); + + self::assertSame($object, $denormalizer->denormalize(\stdClass::class, ['name' => 'name'], $context)); + } + + public function testDenormalizeWithNotCompliantPolicy() + { + $object = new \stdClass(); + + $factory = function () use ($object) { + return $object; + }; + + /** @var DenormalizerContextInterface|MockObject $context */ + $context = $this->getMockByCalls(DenormalizerContextInterface::class, [ + Call::create('getAllowedAdditionalFields')->with()->willReturn(null), + ]); + + /** @var FieldDenormalizerInterface|MockObject $nameFieldDenormalizer */ + $nameFieldDenormalizer = $this->getMockByCalls(FieldDenormalizerInterface::class); + + /** @var PolicyInterface|MockObject $policy */ + $policy = $this->getMockByCalls(PolicyInterface::class, [ + Call::create('isCompliant')->with($context, $object)->willReturn(false), + ]); + + $denormalizationNameFieldMapping = $this->getDenormalizationFieldMappingWithPolicy( + 'name', + $nameFieldDenormalizer, + $policy + ); + + /** @var DenormalizationFieldMappingInterface|MockObject $denormalizationValueFieldMapping */ + $denormalizationValueFieldMapping = $this->getMockByCalls(DenormalizationFieldMappingInterface::class, [ + Call::create('getName')->with()->willReturn('value'), + ]); + + /** @var DenormalizationObjectMappingInterface|MockObject $objectMapping */ + $objectMapping = $this->getMockByCalls(DenormalizationObjectMappingInterface::class, [ + Call::create('getDenormalizationFactory')->with('', null)->willReturn($factory), + Call::create('getDenormalizationFieldMappings')->with('', null)->willReturn([ + $denormalizationNameFieldMapping, + $denormalizationValueFieldMapping, + ]), + ]); + + /** @var DenormalizerObjectMappingRegistryInterface|MockObject $registry */ + $registry = $this->getMockByCalls(DenormalizerObjectMappingRegistryInterface::class, [ + Call::create('getObjectMapping')->with(\stdClass::class)->willReturn($objectMapping), + ]); + + /** @var LoggerInterface|MockObject $logger */ + $logger = $this->getMockByCalls(LoggerInterface::class); + + $denormalizer = new Denormalizer($registry, $logger); + + self::assertSame($object, $denormalizer->denormalize(\stdClass::class, ['name' => 'name'], $context)); + } + + /** + * @param string $name + * @param FieldDenormalizerInterface $fieldDenormalizer + * @param PolicyInterface $policy + * @param array $groups + * + * @return DenormalizationFieldMappingInterface + * + * @todo remove as soon getPolicy() is part of the mapping interface + */ + private function getDenormalizationFieldMappingWithPolicy( + string $name, + FieldDenormalizerInterface $fieldDenormalizer, + PolicyInterface $policy, + array $groups = [] + ): DenormalizationFieldMappingInterface { + return new class($name, $fieldDenormalizer, $policy, $groups) implements DenormalizationFieldMappingInterface { + private $name; + private $fieldDenormalizer; + private $policy; + private $groups; + + public function __construct($name, $fieldDenormalizer, $policy, $groups) + { + $this->name = $name; + $this->fieldDenormalizer = $fieldDenormalizer; + $this->policy = $policy; + $this->groups = $groups; + } + + public function getName(): string + { + return $this->name; + } + + /** + * @return string[] + * + * @deprecated + */ + public function getGroups(): array + { + return $this->groups; + } + + /** + * @return FieldDenormalizerInterface + */ + public function getFieldDenormalizer(): FieldDenormalizerInterface + { + return $this->fieldDenormalizer; + } + + public function getPolicy(): PolicyInterface + { + return $this->policy; + } + }; + } } diff --git a/tests/Mapping/DenormalizationFieldMappingBuilderTest.php b/tests/Mapping/DenormalizationFieldMappingBuilderTest.php index 51b29e8..b695f4c 100644 --- a/tests/Mapping/DenormalizationFieldMappingBuilderTest.php +++ b/tests/Mapping/DenormalizationFieldMappingBuilderTest.php @@ -14,6 +14,8 @@ use Chubbyphp\Deserialization\Denormalizer\Relation\ReferenceManyFieldDenormalizer; use Chubbyphp\Deserialization\Denormalizer\Relation\ReferenceOneFieldDenormalizer; use Chubbyphp\Deserialization\Mapping\DenormalizationFieldMappingBuilder; +use Chubbyphp\Deserialization\Policy\NullPolicy; +use Chubbyphp\Deserialization\Policy\PolicyInterface; use Chubbyphp\Mock\MockByCallsTrait; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -27,6 +29,21 @@ class DenormalizationFieldMappingBuilderTest extends TestCase { use MockByCallsTrait; + public function testGetMappingWithDenormalizer() + { + /** @var FieldDenormalizerInterface|MockObject $fieldDenormalizer */ + $fieldDenormalizer = $this->getMockByCalls(FieldDenormalizerInterface::class); + + $fieldMapping = DenormalizationFieldMappingBuilder::create('name', false, $fieldDenormalizer)->getMapping(); + + self::assertSame('name', $fieldMapping->getName()); + self::assertSame([], $fieldMapping->getGroups()); + + self::assertSame($fieldDenormalizer, $fieldMapping->getFieldDenormalizer()); + + self::assertInstanceOf(NullPolicy::class, $fieldMapping->getPolicy()); + } + public function testGetDefaultMapping() { $fieldMapping = DenormalizationFieldMappingBuilder::create('name')->getMapping(); @@ -42,6 +59,8 @@ public function testGetDefaultMapping() $reflectionObject->setAccessible(true); self::assertFalse($reflectionObject->getValue($fieldDenormalizer)); + + self::assertInstanceOf(NullPolicy::class, $fieldMapping->getPolicy()); } public function testGetDefaultMappingWithEmptyToNull() @@ -59,6 +78,8 @@ public function testGetDefaultMappingWithEmptyToNull() $reflectionObject->setAccessible(true); self::assertTrue($reflectionObject->getValue($fieldDenormalizer)); + + self::assertInstanceOf(NullPolicy::class, $fieldMapping->getPolicy()); } public function testGetDefaultMappingForCallback() @@ -68,6 +89,8 @@ public function testGetDefaultMappingForCallback() self::assertSame('name', $fieldMapping->getName()); self::assertSame([], $fieldMapping->getGroups()); self::assertInstanceOf(CallbackFieldDenormalizer::class, $fieldMapping->getFieldDenormalizer()); + + self::assertInstanceOf(NullPolicy::class, $fieldMapping->getPolicy()); } public function testGetDefaultMappingForConvertType() @@ -88,6 +111,8 @@ public function testGetDefaultMappingForConvertType() $reflectionObject->setAccessible(true); self::assertFalse($reflectionObject->getValue($fieldDenormalizer)); + + self::assertInstanceOf(NullPolicy::class, $fieldMapping->getPolicy()); } public function testGetDefaultMappingForConvertTypeWithEmptyToNull() @@ -109,6 +134,8 @@ public function testGetDefaultMappingForConvertTypeWithEmptyToNull() $reflectionObject->setAccessible(true); self::assertTrue($reflectionObject->getValue($fieldDenormalizer)); + + self::assertInstanceOf(NullPolicy::class, $fieldMapping->getPolicy()); } public function testGetDefaultMappingForDateTime() @@ -126,6 +153,8 @@ public function testGetDefaultMappingForDateTime() $reflectionObject->setAccessible(true); self::assertFalse($reflectionObject->getValue($fieldDenormalizer)); + + self::assertInstanceOf(NullPolicy::class, $fieldMapping->getPolicy()); } public function testGetDefaultMappingForDateTimeWithEmptyToNull() @@ -143,6 +172,8 @@ public function testGetDefaultMappingForDateTimeWithEmptyToNull() $reflectionObject->setAccessible(true); self::assertTrue($reflectionObject->getValue($fieldDenormalizer)); + + self::assertInstanceOf(NullPolicy::class, $fieldMapping->getPolicy()); } public function testGetDefaultMappingForDateTimeWithTimezone() @@ -152,6 +183,8 @@ public function testGetDefaultMappingForDateTimeWithTimezone() self::assertSame('name', $fieldMapping->getName()); self::assertSame([], $fieldMapping->getGroups()); self::assertInstanceOf(DateTimeFieldDenormalizer::class, $fieldMapping->getFieldDenormalizer()); + + self::assertInstanceOf(NullPolicy::class, $fieldMapping->getPolicy()); } public function testGetDefaultMappingForEmbedMany() @@ -161,6 +194,8 @@ public function testGetDefaultMappingForEmbedMany() self::assertSame('name', $fieldMapping->getName()); self::assertSame([], $fieldMapping->getGroups()); self::assertInstanceOf(EmbedManyFieldDenormalizer::class, $fieldMapping->getFieldDenormalizer()); + + self::assertInstanceOf(NullPolicy::class, $fieldMapping->getPolicy()); } public function testGetDefaultMappingForEmbedOne() @@ -170,6 +205,8 @@ public function testGetDefaultMappingForEmbedOne() self::assertSame('name', $fieldMapping->getName()); self::assertSame([], $fieldMapping->getGroups()); self::assertInstanceOf(EmbedOneFieldDenormalizer::class, $fieldMapping->getFieldDenormalizer()); + + self::assertInstanceOf(NullPolicy::class, $fieldMapping->getPolicy()); } public function testGetDefaultMappingForReferenceMany() @@ -179,6 +216,8 @@ public function testGetDefaultMappingForReferenceMany() self::assertSame('name', $fieldMapping->getName()); self::assertSame([], $fieldMapping->getGroups()); self::assertInstanceOf(ReferenceManyFieldDenormalizer::class, $fieldMapping->getFieldDenormalizer()); + + self::assertInstanceOf(NullPolicy::class, $fieldMapping->getPolicy()); } public function testGetDefaultMappingForReferenceOne() @@ -196,6 +235,8 @@ public function testGetDefaultMappingForReferenceOne() $reflectionObject->setAccessible(true); self::assertFalse($reflectionObject->getValue($fieldDenormalizer)); + + self::assertInstanceOf(NullPolicy::class, $fieldMapping->getPolicy()); } public function testGetDefaultMappingForReferenceOneWithEmptyToNull() @@ -217,6 +258,8 @@ function () {}, $reflectionObject->setAccessible(true); self::assertTrue($reflectionObject->getValue($fieldDenormalizer)); + + self::assertInstanceOf(NullPolicy::class, $fieldMapping->getPolicy()); } public function testGetMapping() @@ -224,14 +267,27 @@ public function testGetMapping() /** @var FieldDenormalizerInterface|MockObject $fieldDenormalizer */ $fieldDenormalizer = $this->getMockByCalls(FieldDenormalizerInterface::class); + /** @var PolicyInterface|MockObject $policy */ + $policy = $this->getMockByCalls(PolicyInterface::class); + + error_clear_last(); + $fieldMapping = DenormalizationFieldMappingBuilder::create('name') ->setGroups(['group1']) ->setFieldDenormalizer($fieldDenormalizer) + ->setPolicy($policy) ->getMapping() ; + $error = error_get_last(); + + self::assertSame(E_USER_DEPRECATED, $error['type']); + self::assertSame('Utilize third parameter of create method instead', $error['message']); + self::assertSame('name', $fieldMapping->getName()); self::assertSame(['group1'], $fieldMapping->getGroups()); self::assertSame($fieldDenormalizer, $fieldMapping->getFieldDenormalizer()); + + self::assertSame($policy, $fieldMapping->getPolicy()); } } diff --git a/tests/Mapping/DenormalizationFieldMappingTest.php b/tests/Mapping/DenormalizationFieldMappingTest.php index 1359d5b..4625b9e 100644 --- a/tests/Mapping/DenormalizationFieldMappingTest.php +++ b/tests/Mapping/DenormalizationFieldMappingTest.php @@ -6,6 +6,7 @@ use Chubbyphp\Deserialization\Denormalizer\FieldDenormalizerInterface; use Chubbyphp\Deserialization\Mapping\DenormalizationFieldMapping; +use Chubbyphp\Deserialization\Policy\PolicyInterface; use Chubbyphp\Mock\MockByCallsTrait; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -48,4 +49,17 @@ public function testGetFieldDenormalizer() self::assertSame($fieldDenormalizer, $fieldMapping->getFieldDenormalizer()); } + + public function testGetPolicy() + { + /** @var FieldDenormalizerInterface|MockObject $fieldDenormalizer */ + $fieldDenormalizer = $this->getMockByCalls(FieldDenormalizerInterface::class); + + /** @var PolicyInterface|MockObject $policy */ + $policy = $this->getMockByCalls(PolicyInterface::class); + + $fieldMapping = new DenormalizationFieldMapping('name', ['group1'], $fieldDenormalizer, $policy); + + self::assertSame($policy, $fieldMapping->getPolicy()); + } } diff --git a/tests/Policy/AndPolicyTest.php b/tests/Policy/AndPolicyTest.php new file mode 100644 index 0000000..ce406e4 --- /dev/null +++ b/tests/Policy/AndPolicyTest.php @@ -0,0 +1,70 @@ +getMockByCalls(DenormalizerContextInterface::class, []); + + /** @var PolicyInterface|MockObject $compliantPolicy1 */ + $compliantPolicy1 = $this->getMockByCalls(PolicyInterface::class, [ + Call::create('isCompliant')->with($context, $object)->willReturn(true), + ]); + + /** @var PolicyInterface|MockObject $compliantPolicy2 */ + $compliantPolicy2 = $this->getMockByCalls(PolicyInterface::class, [ + Call::create('isCompliant')->with($context, $object)->willReturn(true), + ]); + + $policy = new AndPolicy([$compliantPolicy1, $compliantPolicy2]); + + self::assertTrue($policy->isCompliant($context, $object)); + } + + public function testIsCompliantReturnsFalseWithNonCompliantPolicy() + { + $object = new \stdClass(); + + /** @var DenormalizerContextInterface|MockObject $context */ + $context = $this->getMockByCalls(DenormalizerContextInterface::class, []); + + /** @var PolicyInterface|MockObject $compliantPolicy1 */ + $compliantPolicy1 = $this->getMockByCalls(PolicyInterface::class, [ + Call::create('isCompliant')->with($context, $object)->willReturn(true), + ]); + + /** @var PolicyInterface|MockObject $nonCompliantPolicy */ + $nonCompliantPolicy = $this->getMockByCalls(PolicyInterface::class, [ + Call::create('isCompliant')->with($context, $object)->willReturn(false), + ]); + + /** @var PolicyInterface|MockObject $notExpectedToBeCalledPolicy */ + $notExpectedToBeCalledPolicy = $this->getMockByCalls(PolicyInterface::class); + + $policy = new AndPolicy([$compliantPolicy1, $nonCompliantPolicy, $notExpectedToBeCalledPolicy]); + + self::assertFalse($policy->isCompliant($context, $object)); + } +} diff --git a/tests/Policy/CallbackPolicyTest.php b/tests/Policy/CallbackPolicyTest.php new file mode 100644 index 0000000..0c682a1 --- /dev/null +++ b/tests/Policy/CallbackPolicyTest.php @@ -0,0 +1,55 @@ +getMockByCalls(DenormalizerContextInterface::class, []); + + $policy = new CallbackPolicy(function ($contextParameter, $objectParameter) use ($context, $object) { + self::assertSame($context, $contextParameter); + self::assertSame($object, $objectParameter); + + return true; + }); + + self::assertTrue($policy->isCompliant($context, $object)); + } + + public function testIsCompliantReturnsFalseIfCallbackReturnsFalse() + { + $object = new \stdClass(); + + /** @var DenormalizerContextInterface|MockObject $context */ + $context = $this->getMockByCalls(DenormalizerContextInterface::class, []); + + $policy = new CallbackPolicy(function ($contextParameter, $objectParameter) use ($context, $object) { + self::assertSame($context, $contextParameter); + self::assertSame($object, $objectParameter); + + return false; + }); + + self::assertFalse($policy->isCompliant($context, $object)); + } +} diff --git a/tests/Policy/GroupPolicyTest.php b/tests/Policy/GroupPolicyTest.php new file mode 100644 index 0000000..77797cf --- /dev/null +++ b/tests/Policy/GroupPolicyTest.php @@ -0,0 +1,128 @@ +getMockByCalls(DenormalizerContextInterface::class); + + $policy = new GroupPolicy([]); + + self::assertTrue($policy->isCompliant($context, $object)); + } + + public function testIsCompliantReturnsTrueWithDefaultValues() + { + $object = new \stdClass(); + + /** @var DenormalizerContextInterface|MockObject $context */ + $context = $this->getDenormalizerContextWithGroupAttribute(null); + + $policy = new GroupPolicy(); + + self::assertTrue($policy->isCompliant($context, $object)); + } + + public function testIsCompliantReturnsTrueIfOneGroupMatches() + { + $object = new \stdClass(); + + /** @var DenormalizerContextInterface|MockObject $context */ + $context = $this->getDenormalizerContextWithGroupAttribute(['group2']); + + $policy = new GroupPolicy(['group1', 'group2']); + + self::assertTrue($policy->isCompliant($context, $object)); + } + + public function testIsCompliantReturnsFalseIfNoGroupsAreSetInContext() + { + $object = new \stdClass(); + + /** @var DenormalizerContextInterface|MockObject $context */ + $context = $this->getDenormalizerContextWithGroupAttribute([]); + + $policy = new GroupPolicy(['group1', 'group2']); + + self::assertFalse($policy->isCompliant($context, $object)); + } + + public function testIsCompliantReturnsFalseIfNoGroupsMatch() + { + $object = new \stdClass(); + + /** @var DenormalizerContextInterface|MockObject $context */ + $context = $this->getDenormalizerContextWithGroupAttribute(['unknownGroup']); + + $policy = new GroupPolicy(['group1', 'group2']); + + self::assertFalse($policy->isCompliant($context, $object)); + } + + /** + * @param array|null $groups + * + * @return DenormalizerContextInterface + */ + private function getDenormalizerContextWithGroupAttribute(array $groups = null): DenormalizerContextInterface + { + return new class($groups) implements DenormalizerContextInterface { + private $groups; + + public function __construct($groups) + { + $this->groups = $groups; + } + + public function getAllowedAdditionalFields() + { + return null; + } + + public function getGroups(): array + { + return []; + } + + public function getRequest() + { + return null; + } + + public function getAttributes(): array + { + return []; + } + + public function getAttribute(string $name, $default = null) + { + return $this->groups ?? $default; + } + + public function withAttribute(string $name, $value): DenormalizerContextInterface + { + return $this; + } + }; + } +} diff --git a/tests/Policy/NullPolicyTest.php b/tests/Policy/NullPolicyTest.php new file mode 100644 index 0000000..c35f8a4 --- /dev/null +++ b/tests/Policy/NullPolicyTest.php @@ -0,0 +1,33 @@ +getMockByCalls(DenormalizerContextInterface::class); + + $policy = new NullPolicy(); + + self::assertTrue($policy->isCompliant($context, $object)); + } +} diff --git a/tests/Policy/OrPolicyTest.php b/tests/Policy/OrPolicyTest.php new file mode 100644 index 0000000..3b09f80 --- /dev/null +++ b/tests/Policy/OrPolicyTest.php @@ -0,0 +1,70 @@ +getMockByCalls(DenormalizerContextInterface::class, []); + + /** @var PolicyInterface|MockObject $nonCompliantPolicy */ + $nonCompliantPolicy = $this->getMockByCalls(PolicyInterface::class, [ + Call::create('isCompliant')->with($context, $object)->willReturn(false), + ]); + + /** @var PolicyInterface|MockObject $compliantPolicy */ + $compliantPolicy = $this->getMockByCalls(PolicyInterface::class, [ + Call::create('isCompliant')->with($context, $object)->willReturn(true), + ]); + + /** @var PolicyInterface|MockObject $notToBeCalledPolicy */ + $notToBeCalledPolicy = $this->getMockByCalls(PolicyInterface::class, []); + + $policy = new OrPolicy([$nonCompliantPolicy, $compliantPolicy, $notToBeCalledPolicy]); + + self::assertTrue($policy->isCompliant($context, $object)); + } + + public function testIsCompliantReturnsFalseIfAllPoliciesReturnFalse() + { + $object = new \stdClass(); + + /** @var DenormalizerContextInterface|MockObject $context */ + $context = $this->getMockByCalls(DenormalizerContextInterface::class, []); + + /** @var PolicyInterface|MockObject $nonCompliantPolicy1 */ + $nonCompliantPolicy1 = $this->getMockByCalls(PolicyInterface::class, [ + Call::create('isCompliant')->with($context, $object)->willReturn(false), + ]); + + /** @var PolicyInterface|MockObject $nonCompliantPolicy2 */ + $nonCompliantPolicy2 = $this->getMockByCalls(PolicyInterface::class, [ + Call::create('isCompliant')->with($context, $object)->willReturn(false), + ]); + + $policy = new OrPolicy([$nonCompliantPolicy1, $nonCompliantPolicy2]); + + self::assertFalse($policy->isCompliant($context, $object)); + } +}