diff --git a/src/Incoming/Hydrator/AbstractDelegateContextualBuilder.php b/src/Incoming/Hydrator/AbstractDelegateContextualBuilder.php index e4c0246..7d0293b 100644 --- a/src/Incoming/Hydrator/AbstractDelegateContextualBuilder.php +++ b/src/Incoming/Hydrator/AbstractDelegateContextualBuilder.php @@ -22,6 +22,37 @@ abstract class AbstractDelegateContextualBuilder extends AbstractDelegateBuilder implements ContextualBuilder { + /** + * Properties + */ + + /** + * Whether or not to provide a fallback empty context, when a `null` context + * is otherwise provided, to make processes simpler by not having to rely on + * null checks of the actual parameter before usage. + * + * @var bool + */ + private $provide_fallback_context = false; + + + /** + * Methods + */ + + /** + * Constructor + * + * @param bool $provide_fallback_context Whether or not to provide a + * fallback empty context, when a `null` context is otherwise provided, to + * make processes simpler by not having to rely on null checks of the + * actual parameter before usage. + */ + protected function __construct(bool $provide_fallback_context = false) + { + $this->provide_fallback_context = $provide_fallback_context; + } + /** * {@inheritdoc} * @@ -34,6 +65,11 @@ public function build($incoming, Map $context = null) { $callable = $this->getDelegate(); + if (null === $context && $this->provide_fallback_context) { + // Provide a non-null context so null checks aren't later required + $context = new Map(); + } + return $callable($incoming, $context); } } diff --git a/src/Incoming/Hydrator/AbstractDelegateContextualBuilderHydrator.php b/src/Incoming/Hydrator/AbstractDelegateContextualBuilderHydrator.php index b3534c1..c282319 100644 --- a/src/Incoming/Hydrator/AbstractDelegateContextualBuilderHydrator.php +++ b/src/Incoming/Hydrator/AbstractDelegateContextualBuilderHydrator.php @@ -26,6 +26,37 @@ abstract class AbstractDelegateContextualBuilderHydrator extends AbstractDelegat ContextualHydrator { + /** + * Properties + */ + + /** + * Whether or not to provide a fallback empty context, when a `null` context + * is otherwise provided, to make processes simpler by not having to rely on + * null checks of the actual parameter before usage. + * + * @var bool + */ + private $provide_fallback_context = false; + + + /** + * Methods + */ + + /** + * Constructor + * + * @param bool $provide_fallback_context Whether or not to provide a + * fallback empty context, when a `null` context is otherwise provided, to + * make processes simpler by not having to rely on null checks of the + * actual parameter before usage. + */ + protected function __construct(bool $provide_fallback_context = false) + { + $this->provide_fallback_context = $provide_fallback_context; + } + /** * {@inheritdoc} * @@ -38,6 +69,11 @@ public function build($incoming, Map $context = null) { $callable = $this->getDelegateBuilder(); + if (null === $context && $this->provide_fallback_context) { + // Provide a non-null context so null checks aren't later required + $context = new Map(); + } + return $callable($incoming, $context); } @@ -54,6 +90,11 @@ public function hydrate($incoming, $model, Map $context = null) { $callable = $this->getDelegateHydrator(); + if (null === $context && $this->provide_fallback_context) { + // Provide a non-null context so null checks aren't later required + $context = new Map(); + } + return $callable($incoming, $model, $context); } } diff --git a/src/Incoming/Hydrator/AbstractDelegateContextualHydrator.php b/src/Incoming/Hydrator/AbstractDelegateContextualHydrator.php index 791639b..84d12ee 100644 --- a/src/Incoming/Hydrator/AbstractDelegateContextualHydrator.php +++ b/src/Incoming/Hydrator/AbstractDelegateContextualHydrator.php @@ -22,6 +22,37 @@ abstract class AbstractDelegateContextualHydrator extends AbstractDelegateHydrator implements ContextualHydrator { + /** + * Properties + */ + + /** + * Whether or not to provide a fallback empty context, when a `null` context + * is otherwise provided, to make processes simpler by not having to rely on + * null checks of the actual parameter before usage. + * + * @var bool + */ + private $provide_fallback_context = false; + + + /** + * Methods + */ + + /** + * Constructor + * + * @param bool $provide_fallback_context Whether or not to provide a + * fallback empty context, when a `null` context is otherwise provided, to + * make processes simpler by not having to rely on null checks of the + * actual parameter before usage. + */ + protected function __construct(bool $provide_fallback_context = false) + { + $this->provide_fallback_context = $provide_fallback_context; + } + /** * {@inheritdoc} * @@ -35,6 +66,11 @@ public function hydrate($incoming, $model, Map $context = null) { $callable = $this->getDelegate(); + if (null === $context && $this->provide_fallback_context) { + // Provide a non-null context so null checks aren't later required + $context = new Map(); + } + return $callable($incoming, $model, $context); } } diff --git a/src/Incoming/Hydrator/Exception/IncompatibleProcessException.php b/src/Incoming/Hydrator/Exception/IncompatibleProcessException.php new file mode 100644 index 0000000..5938faa --- /dev/null +++ b/src/Incoming/Hydrator/Exception/IncompatibleProcessException.php @@ -0,0 +1,110 @@ +input_transformer = $input_transformer ?: new StructureBuilderTransformer(); $this->hydrator_factory = $hydrator_factory; $this->builder_factory = $builder_factory; $this->always_hydrate_after_building = $always_hydrate_after_building; + $this->require_contextual_processing_compatibility = $require_contextual_processing_compatibility; } /** @@ -189,6 +203,35 @@ public function setAlwaysHydrateAfterBuilding(bool $always_hydrate_after_buildin return $this; } + /** + * Get the value of the configuration flag that denotes whether processing + * (hydration/building) should require contextual compatibility when a + * context is provided. + * + * @return bool The value of the flag. + */ + public function getRequireContextualProcessingCompatibility(): bool + { + return $this->require_contextual_processing_compatibility; + } + + /** + * Set the value of the configuration flag that denotes whether processing + * (hydration/building) should require contextual compatibility when a + * context is provided. + * + * @param bool $require_contextual_processing_compatibility Whether or not + * to require contextual processing compatibility when a context is + * provided. + * @return $this This instance. + */ + public function setRequireContextualProcessingCompatibility(bool $require_contextual_processing_compatibility): self + { + $this->require_contextual_processing_compatibility = $require_contextual_processing_compatibility; + + return $this; + } + /** * {@inheritdoc} * @@ -277,9 +320,9 @@ protected function transformInput($input_data) */ protected function hydrateModel($input_data, $model, Hydrator $hydrator = null, Map $context = null) { - if (null === $hydrator) { - $hydrator = $this->getHydratorForModel($model); - } + $hydrator = $hydrator ?: $this->getHydratorForModel($model); + + $this->enforceProcessCompatibility(($hydrator instanceof ContextualHydrator), (null !== $context), $hydrator); if ($hydrator instanceof ContextualHydrator) { return $hydrator->hydrate($input_data, $model, $context); @@ -303,9 +346,9 @@ protected function hydrateModel($input_data, $model, Hydrator $hydrator = null, */ protected function buildModel($input_data, string $type, Builder $builder = null, Map $context = null) { - if (null === $builder) { - $builder = $this->getBuilderForType($type); - } + $builder = $builder ?: $this->getBuilderForType($type); + + $this->enforceProcessCompatibility(($builder instanceof ContextualBuilder), (null !== $context), $builder); if ($builder instanceof ContextualBuilder) { return $builder->build($input_data, $context); @@ -347,4 +390,25 @@ protected function getBuilderForType(string $type): Builder return $this->builder_factory->buildForType($type); } + + /** + * Enforce that a provided process (hydrator, builder, etc) is compatible + * with the processing strategy being used. + * + * @param bool $is_context_compatible Whether or not the process is + * compatible with contexts. + * @param bool $context_provided Whether or not a context has been provided + * in the process. + * @param object|null $process The process to enforce compatibility for. + * @throws IncompatibleProcessException If the builder isn't compatible with + * the given process strategy. + * @return void + */ + protected function enforceProcessCompatibility(bool $is_context_compatible, bool $context_provided, $process = null) + { + if ($context_provided && !$is_context_compatible + && $this->require_contextual_processing_compatibility) { + throw IncompatibleProcessException::forRequiredContextCompatibility($process); + } + } } diff --git a/tests/Incoming/Test/Hydrator/AbstractDelegateBuilderTest.php b/tests/Incoming/Test/Hydrator/AbstractDelegateBuilderTest.php index 0bcf850..fb5db49 100644 --- a/tests/Incoming/Test/Hydrator/AbstractDelegateBuilderTest.php +++ b/tests/Incoming/Test/Hydrator/AbstractDelegateBuilderTest.php @@ -32,7 +32,7 @@ private function getMockDelegateBuilder(callable $delegate): AbstractDelegateBui ->setMethods([AbstractDelegateBuilder::DEFAULT_DELEGATE_METHOD_NAME]) ->getMock(); - $mock->expects($this->any()) + $mock->expects($this->atLeastOnce()) ->method(AbstractDelegateBuilder::DEFAULT_DELEGATE_METHOD_NAME) ->will($this->returnCallback($delegate)); diff --git a/tests/Incoming/Test/Hydrator/AbstractDelegateContextualBuilderHydratorTest.php b/tests/Incoming/Test/Hydrator/AbstractDelegateContextualBuilderHydratorTest.php index b74bd9f..054b102 100644 --- a/tests/Incoming/Test/Hydrator/AbstractDelegateContextualBuilderHydratorTest.php +++ b/tests/Incoming/Test/Hydrator/AbstractDelegateContextualBuilderHydratorTest.php @@ -26,24 +26,31 @@ class AbstractDelegateContextualBuilderHydratorTest extends TestCase * Helpers */ - private function getMockDelegateBuilderHydrator(callable $delegate): AbstractDelegateContextualBuilderHydrator - { - $mock = $this->getMockBuilder(AbstractDelegateContextualBuilderHydrator::class) - ->setMethods([ - AbstractDelegateBuilderHydrator::DEFAULT_DELEGATE_BUILD_METHOD_NAME, - AbstractDelegateBuilderHydrator::DEFAULT_DELEGATE_HYDRATE_METHOD_NAME - ]) - ->getMock(); - - $mock->expects($this->any()) - ->method(AbstractDelegateBuilderHydrator::DEFAULT_DELEGATE_BUILD_METHOD_NAME) - ->will($this->returnCallback($delegate)); - - $mock->expects($this->any()) - ->method(AbstractDelegateBuilderHydrator::DEFAULT_DELEGATE_HYDRATE_METHOD_NAME) - ->will($this->returnCallback($delegate)); - - return $mock; + private function getMockDelegateBuilderHydrator( + callable $delegate, + bool $provide_fallback_context = false + ): AbstractDelegateContextualBuilderHydrator { + return new class($delegate, $provide_fallback_context) extends AbstractDelegateContextualBuilderHydrator + { + private $delegate; + + public function __construct(callable $delegate, bool $provide_fallback_context) + { + parent::__construct($provide_fallback_context); + + $this->delegate = $delegate; + } + + protected function buildModel($incoming, Map $context = null) + { + return ($this->delegate)($incoming, $context); + } + + protected function hydrateModel($incoming, $model, Map $context = null) + { + return ($this->delegate)($incoming, $model, $context); + } + }; } @@ -88,6 +95,23 @@ public function testBuild() $this->assertSame($test_context['timezone']->getName(), $built->getTimezone()->getName()); } + public function testBuildProvidesNonNullContext() + { + $this->getMockDelegateBuilderHydrator( + function (array $incoming, Map $context = null) { + $this->assertNotNull($context); + }, + true + )->build([], null); + + $this->getMockDelegateBuilderHydrator( + function (array $incoming, Map $context = null) { + $this->assertNull($context); + }, + false + )->build([], null); + } + public function testHydrate() { $test_input_data = Map::fromArray([ @@ -123,4 +147,23 @@ public function testHydrate() $this->assertSame($test_input_data['day'], (int) $hydrated->format('j')); $this->assertSame($test_context['timezone']->getName(), $hydrated->getTimezone()->getName()); } + + public function testHydrateProvidesNonNullContext() + { + $test_model = new DateTime(); + + $this->getMockDelegateBuilderHydrator( + function (array $incoming, DateTime $model, Map $context = null) { + $this->assertNotNull($context); + }, + true + )->hydrate([], $test_model, null); + + $this->getMockDelegateBuilderHydrator( + function (array $incoming, DateTime $model, Map $context = null) { + $this->assertNull($context); + }, + false + )->hydrate([], $test_model, null); + } } diff --git a/tests/Incoming/Test/Hydrator/AbstractDelegateContextualBuilderTest.php b/tests/Incoming/Test/Hydrator/AbstractDelegateContextualBuilderTest.php index fb625b8..36502c8 100644 --- a/tests/Incoming/Test/Hydrator/AbstractDelegateContextualBuilderTest.php +++ b/tests/Incoming/Test/Hydrator/AbstractDelegateContextualBuilderTest.php @@ -26,17 +26,26 @@ class AbstractDelegateContextualBuilderTest extends TestCase * Helpers */ - private function getMockDelegateBuilder(callable $delegate): AbstractDelegateContextualBuilder - { - $mock = $this->getMockBuilder(AbstractDelegateContextualBuilder::class) - ->setMethods([AbstractDelegateBuilder::DEFAULT_DELEGATE_METHOD_NAME]) - ->getMock(); - - $mock->expects($this->any()) - ->method(AbstractDelegateBuilder::DEFAULT_DELEGATE_METHOD_NAME) - ->will($this->returnCallback($delegate)); + private function getMockDelegateBuilder( + callable $delegate, + bool $provide_fallback_context = false + ): AbstractDelegateContextualBuilder { + return new class($delegate, $provide_fallback_context) extends AbstractDelegateContextualBuilder + { + private $delegate; + + public function __construct(callable $delegate, bool $provide_fallback_context) + { + parent::__construct($provide_fallback_context); + + $this->delegate = $delegate; + } - return $mock; + protected function buildModel($incoming, Map $context = null) + { + return ($this->delegate)($incoming, $context); + } + }; } @@ -80,4 +89,21 @@ public function testBuild() $this->assertSame($test_input_data['day'], (int) $built->format('j')); $this->assertSame($test_context['timezone']->getName(), $built->getTimezone()->getName()); } + + public function testBuildProvidesNonNullContext() + { + $this->getMockDelegateBuilder( + function (array $incoming, Map $context = null) { + $this->assertNotNull($context); + }, + true + )->build([], null); + + $this->getMockDelegateBuilder( + function (array $incoming, Map $context = null) { + $this->assertNull($context); + }, + false + )->build([], null); + } } diff --git a/tests/Incoming/Test/Hydrator/AbstractDelegateContextualHydratorTest.php b/tests/Incoming/Test/Hydrator/AbstractDelegateContextualHydratorTest.php index 0f36476..9d166ff 100644 --- a/tests/Incoming/Test/Hydrator/AbstractDelegateContextualHydratorTest.php +++ b/tests/Incoming/Test/Hydrator/AbstractDelegateContextualHydratorTest.php @@ -26,17 +26,26 @@ class AbstractDelegateContextualHydratorTest extends TestCase * Helpers */ - private function getMockDelegateHydrator(callable $delegate): AbstractDelegateContextualHydrator - { - $mock = $this->getMockBuilder(AbstractDelegateContextualHydrator::class) - ->setMethods([AbstractDelegateHydrator::DEFAULT_DELEGATE_METHOD_NAME]) - ->getMock(); - - $mock->expects($this->any()) - ->method(AbstractDelegateHydrator::DEFAULT_DELEGATE_METHOD_NAME) - ->will($this->returnCallback($delegate)); + private function getMockDelegateHydrator( + callable $delegate, + bool $provide_fallback_context = false + ): AbstractDelegateContextualHydrator { + return new class($delegate, $provide_fallback_context) extends AbstractDelegateContextualHydrator + { + private $delegate; + + public function __construct(callable $delegate, bool $provide_fallback_context) + { + parent::__construct($provide_fallback_context); + + $this->delegate = $delegate; + } - return $mock; + protected function hydrateModel($incoming, $model, Map $context = null) + { + return ($this->delegate)($incoming, $model, $context); + } + }; } @@ -79,4 +88,23 @@ public function testHydrate() $this->assertSame($test_input_data['day'], (int) $hydrated->format('j')); $this->assertSame($test_context['timezone']->getName(), $hydrated->getTimezone()->getName()); } + + public function testHydrateProvidesNonNullContext() + { + $test_model = new DateTime(); + + $this->getMockDelegateHydrator( + function (array $incoming, DateTime $model, Map $context = null) { + $this->assertNotNull($context); + }, + true + )->hydrate([], $test_model, null); + + $this->getMockDelegateHydrator( + function (array $incoming, DateTime $model, Map $context = null) { + $this->assertNull($context); + }, + false + )->hydrate([], $test_model, null); + } } diff --git a/tests/Incoming/Test/Hydrator/AbstractDelegateHydratorTest.php b/tests/Incoming/Test/Hydrator/AbstractDelegateHydratorTest.php index bde4285..3d3e721 100644 --- a/tests/Incoming/Test/Hydrator/AbstractDelegateHydratorTest.php +++ b/tests/Incoming/Test/Hydrator/AbstractDelegateHydratorTest.php @@ -32,7 +32,7 @@ private function getMockDelegateHydrator(callable $delegate): AbstractDelegateHy ->setMethods([AbstractDelegateHydrator::DEFAULT_DELEGATE_METHOD_NAME]) ->getMock(); - $mock->expects($this->any()) + $mock->expects($this->atLeastOnce()) ->method(AbstractDelegateHydrator::DEFAULT_DELEGATE_METHOD_NAME) ->will($this->returnCallback($delegate)); diff --git a/tests/Incoming/Test/Hydrator/Exception/IncompatibleProcessExceptionTest.php b/tests/Incoming/Test/Hydrator/Exception/IncompatibleProcessExceptionTest.php new file mode 100644 index 0000000..9708253 --- /dev/null +++ b/tests/Incoming/Test/Hydrator/Exception/IncompatibleProcessExceptionTest.php @@ -0,0 +1,76 @@ +assertInstanceOf(Exception::class, $exception); + $this->assertInstanceOf(IncompatibleProcessException::class, $exception); + $this->assertSame(IncompatibleProcessException::CODE_FOR_REQUIRED_CONTEXT_COMPATIBILITY, $exception->getCode()); + } + + public function testForRequiredContextCompatibilityWithHydratorProcess() + { + $process = $this->createMock(Hydrator::class); + + $exception = IncompatibleProcessException::forRequiredContextCompatibility($process); + + $this->assertInstanceOf(Exception::class, $exception); + $this->assertInstanceOf(IncompatibleProcessException::class, $exception); + $this->assertSame( + IncompatibleProcessException::CODE_FOR_REQUIRED_CONTEXT_COMPATIBILITY + + IncompatibleProcessException::CODE_FOR_HYDRATOR, + $exception->getCode() + ); + } + + public function testForRequiredContextCompatibilityWithBuilderProcess() + { + $process = $this->createMock(Builder::class); + + $exception = IncompatibleProcessException::forRequiredContextCompatibility($process); + + $this->assertInstanceOf(Exception::class, $exception); + $this->assertInstanceOf(IncompatibleProcessException::class, $exception); + $this->assertSame( + IncompatibleProcessException::CODE_FOR_REQUIRED_CONTEXT_COMPATIBILITY + + IncompatibleProcessException::CODE_FOR_BUILDER, + $exception->getCode() + ); + } + + public function testForRequiredContextCompatibilityWithProcessAndExceptionArgs() + { + $process = $this->createMock(Hydrator::class); + $code = 1337; + $previous = new Exception(); + + $exception = IncompatibleProcessException::forRequiredContextCompatibility($process, $code, $previous); + + $this->assertInstanceOf(Exception::class, $exception); + $this->assertInstanceOf(IncompatibleProcessException::class, $exception); + $this->assertSame($code, $exception->getCode()); + $this->assertSame($previous, $exception->getPrevious()); + } +} diff --git a/tests/Incoming/Test/ProcessorTest.php b/tests/Incoming/Test/ProcessorTest.php index 0eb70c9..9cb9946 100644 --- a/tests/Incoming/Test/ProcessorTest.php +++ b/tests/Incoming/Test/ProcessorTest.php @@ -16,6 +16,7 @@ use Incoming\Hydrator\BuilderFactory; use Incoming\Hydrator\ContextualBuilder; use Incoming\Hydrator\ContextualHydrator; +use Incoming\Hydrator\Exception\IncompatibleProcessException; use Incoming\Hydrator\Exception\UnresolvableBuilderException; use Incoming\Hydrator\Exception\UnresolvableHydratorException; use Incoming\Hydrator\Hydrator; @@ -164,6 +165,20 @@ public function testGetSetAlwaysHydrateAfterBuilding() $this->assertSame($test_value, $processor->getAlwaysHydrateAfterBuilding()); } + public function testGetSetRequireContextualProcessingCompatibility() + { + $test_value = true; + + $processor = new Processor(); + + $initial_value = $processor->getRequireContextualProcessingCompatibility(); + + $processor->setRequireContextualProcessingCompatibility($test_value); + + $this->assertNotSame($initial_value, $processor->getRequireContextualProcessingCompatibility()); + $this->assertSame($test_value, $processor->getRequireContextualProcessingCompatibility()); + } + public function testProcessForModel() { $test_input_data = [ @@ -411,4 +426,32 @@ public function testProcessForTypeWithAlwaysUseHydratorAndUnresolvableHydrator() $processor->processForType($test_input_data, $test_type, $test_builder); } + + public function testProcessForModelWithRequireContextualProcessingCompatibilityAndIncompatibleHydrator() + { + $test_model = new stdClass(); + $test_hydrator = $this->createMock(Hydrator::class); + $test_context = new Map(); + + $processor = new Processor(); + $processor->setRequireContextualProcessingCompatibility(true); + + $this->expectException(IncompatibleProcessException::class); + + $processor->processForModel([], $test_model, $test_hydrator, $test_context); + } + + public function testProcessForTypeWithRequireContextualProcessingCompatibilityAndIncompatibleBuilder() + { + $test_type = stdClass::class; + $test_builder = $this->createMock(Builder::class); + $test_context = new Map(); + + $processor = new Processor(); + $processor->setRequireContextualProcessingCompatibility(true); + + $this->expectException(IncompatibleProcessException::class); + + $processor->processForType([], $test_type, $test_builder, null, $test_context); + } }