diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8a085ca --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# CHANGELOG + +## 0.2.0 + +### Features + +- Introduced a new `AbstractDelegateHydrator` class to allow for implementing a hydrator while using a delegate callback + - While this facilitates simple method delegation, its real design was to allow for the use of type-hinted hydrators + that could circumvent PHP's type-system limitations. + - For more info, read the class doc-block of the new `AbstractDelegateHydrator` class diff --git a/Makefile b/Makefile index 4cc9892..fd5838b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -all: install test lint checkstyle +all: install test lint check-style install: composer install --prefer-dist diff --git a/README.md b/README.md index 8e1fbc2..14c5157 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,36 @@ $user = $incoming->process( // ... ``` +Missing type hints? PHP's type-system's restrictions can be circumvented!: + +```php +class UserHydrator extends Incoming\Hydrator\AbstractDelegateHydrator +{ + // Boom! Type-hintable arguments! + // (For more info, see the `AbstractDelegateHydrator` class doc-block) + public function hydrateModel(Incoming\Structure\Map $input, User $model) + { + $model->setName($input['name']); + // ... + + return $model; + } +} + +// Create our incoming processor +$incoming = new Incoming\Processor(); + +// Process our raw form/request input into a User model +$user = $incoming->process( + $_POST, // Our HTTP form-data array + new User(), // Our model to hydrate + new UserHydrator() // The hydrator above +); + +// Validate and save the user +// ... +``` + ## Wait, what? Why not just use "x" or "y"? diff --git a/src/Incoming/Hydrator/AbstractDelegateHydrator.php b/src/Incoming/Hydrator/AbstractDelegateHydrator.php new file mode 100644 index 0000000..3b31392 --- /dev/null +++ b/src/Incoming/Hydrator/AbstractDelegateHydrator.php @@ -0,0 +1,100 @@ +` + * + * @link http://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science) + * @link http://en.wikipedia.org/wiki/Generic_programming + */ +abstract class AbstractDelegateHydrator implements HydratorInterface +{ + + /** + * Constants + */ + + /** + * The name of the default delegate method + * + * @type string + */ + const DEFAULT_DELEGATE_METHOD_NAME = 'hydrateModel'; + + + /** + * Methods + */ + + /** + * {@inheritdoc} + * + * @param mixed $incoming The input data + * @param mixed $model The model to hydrate + * @return mixed The hydrated model + */ + public function hydrate($incoming, $model) + { + return call_user_func( + $this->getDelegate(), + $incoming, + $model + ); + } + + /** + * Get the delegate hydration callable + * + * Override this method if a custom delegate is desired + * + * @return callable The delegate hydrator callable + */ + protected function getDelegate() + { + $delegate = [$this, static::DEFAULT_DELEGATE_METHOD_NAME]; + + if (!is_callable($delegate, false, $callable_name)) { + throw InvalidDelegateException::forNonCallable($callable_name); + } + + return $delegate; + } + + /** + * The delegate hydrate method + * + * This doc-block and commented out abstract method is provided here to show + * what the delegate method signature WOULD be if PHP allowed the proper + * typing support to enable a generic definition in this manner + * + * See the class description for more info + * + * @param IncomingDataType $incoming The input data + * @param ModelType $model The model to hydrate + * @return ModelType The hydrated model + */ + // abstract protected function hydrateModel(IncomingDataType $incoming, ModelType $model); +} diff --git a/src/Incoming/Hydrator/Exception/InvalidDelegateException.php b/src/Incoming/Hydrator/Exception/InvalidDelegateException.php new file mode 100644 index 0000000..081be1a --- /dev/null +++ b/src/Incoming/Hydrator/Exception/InvalidDelegateException.php @@ -0,0 +1,93 @@ +getMockBuilder('Incoming\Hydrator\AbstractDelegateHydrator') + ->setMethods([AbstractDelegateHydrator::DEFAULT_DELEGATE_METHOD_NAME]) + ->getMock(); + + $mock->expects($this->any()) + ->method(AbstractDelegateHydrator::DEFAULT_DELEGATE_METHOD_NAME) + ->will($this->returnCallback($delegate)); + + return $mock; + } + + + /** + * Tests + */ + + public function testHydrate() + { + $test_input_data = Map::fromArray([ + 'year' => 1983, + 'month' => 1, + 'day' => 2, + ]); + $test_model = new DateTime(); + + $test_delegate_callable = function (Map $incoming, DateTime $model) { + $model->setDate( + $incoming->get('year'), + $incoming->get('month'), + $incoming->get('day') + ); + + return $model; + }; + + $test_hydrator = $this->getMockDelegateHydrator($test_delegate_callable); + + $hydrated = $test_hydrator->hydrate($test_input_data, $test_model); + + $this->assertEquals($test_model, $hydrated); + $this->assertSame($test_input_data['year'], (int) $hydrated->format('Y')); + $this->assertSame($test_input_data['month'], (int) $hydrated->format('m')); + $this->assertSame($test_input_data['day'], (int) $hydrated->format('j')); + } + + /** + * @expectedException Incoming\Hydrator\Exception\InvalidDelegateException + */ + public function testHydrateWithNonCallableThrowsException() + { + $mock_hydrator = new MockDelegateHydrator(); + + $mock_hydrator->hydrate([], new DateTime()); + } + + /** + * @expectedException PHPUnit_Framework_Error + */ + public function testHydrateWithImproperTypesCausesTypeError() + { + $test_delegate_callable = function (Map $incoming, DateTime $model) { + }; + + $test_hydrator = $this->getMockDelegateHydrator($test_delegate_callable); + + $test_hydrator->hydrate([], new DateTime()); + } +} diff --git a/tests/Incoming/Test/Hydrator/Exception/InvalidDelegateExceptionTest.php b/tests/Incoming/Test/Hydrator/Exception/InvalidDelegateExceptionTest.php new file mode 100644 index 0000000..0082825 --- /dev/null +++ b/tests/Incoming/Test/Hydrator/Exception/InvalidDelegateExceptionTest.php @@ -0,0 +1,56 @@ +assertTrue($exception instanceof Exception); + $this->assertTrue($exception instanceof InvalidDelegateException); + $this->assertSame(InvalidDelegateException::CODE_FOR_NON_CALLABLE, $exception->getCode()); + } + + public function testForNonCallableWithName() + { + $non_callable_name = 'someNonExistentFunction'; + + $exception = InvalidDelegateException::forNonCallable($non_callable_name); + + $this->assertTrue($exception instanceof Exception); + $this->assertTrue($exception instanceof InvalidDelegateException); + $this->assertSame(InvalidDelegateException::CODE_FOR_NON_CALLABLE, $exception->getCode()); + } + + public function testForNonCallableWithNameAndExceptionArgs() + { + $non_callable_name = 'someNonExistentFunction'; + $code = 1337; + $previous = new Exception(); + + $exception = InvalidDelegateException::forNonCallable($non_callable_name, $code, $previous); + + $this->assertTrue($exception instanceof Exception); + $this->assertTrue($exception instanceof InvalidDelegateException); + $this->assertSame($code, $exception->getCode()); + $this->assertSame($previous, $exception->getPrevious()); + } +} diff --git a/tests/Incoming/Test/Hydrator/MockDelegateHydrator.php b/tests/Incoming/Test/Hydrator/MockDelegateHydrator.php new file mode 100644 index 0000000..5e8373f --- /dev/null +++ b/tests/Incoming/Test/Hydrator/MockDelegateHydrator.php @@ -0,0 +1,20 @@ +