Skip to content

Commit

Permalink
Merge branch 'develop'. Prepare release v0.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Rican7 committed Apr 4, 2015
2 parents 6e8f2d8 + 02b982b commit bbfa8c0
Show file tree
Hide file tree
Showing 8 changed files with 408 additions and 1 deletion.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
all: install test lint checkstyle
all: install test lint check-style

install:
composer install --prefer-dist
Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"?

Expand Down
100 changes: 100 additions & 0 deletions src/Incoming/Hydrator/AbstractDelegateHydrator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php
/**
* Incoming
*
* @author Trevor Suarez (Rican7)
* @copyright (c) Trevor Suarez
* @link https://github.com/Rican7/incoming
* @license MIT
*/

namespace Incoming\Hydrator;

use Incoming\Hydrator\Exception\InvalidDelegateException;

/**
* AbstractDelegateHydrator
*
* An abstract hydrator that allows for the hydration to be delegated to another
* callable. By default, a named method is attempted to be found, but any
* callable could be returned through overrides.
*
* This enables a lot of interesting uses, most notably this allows hydrators to
* be created that have strongly type-hinted hydration arguments while still
* perfectly satisfying the `HydratorInterface`. Essentially this allows the
* bypassing of the type variance rules enforced by PHP in a way that provides a
* generics-like definition. Ultimately, if/when PHP gets generics this will no
* longer be necessary, as one could simply implement a hydrator using typed
* arguments like: `HydratorInterface<IncomingDataType, ModelType>`
*
* @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);
}
93 changes: 93 additions & 0 deletions src/Incoming/Hydrator/Exception/InvalidDelegateException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php
/**
* Incoming
*
* @author Trevor Suarez (Rican7)
* @copyright (c) Trevor Suarez
* @link https://github.com/Rican7/incoming
* @license MIT
*/

namespace Incoming\Hydrator\Exception;

use BadFunctionCallException;
use Exception;

/**
* InvalidDelegateException
*
* An exception to be thrown when an invalid delegate method, function, or
* callback is provided to a caller
*/
class InvalidDelegateException extends BadFunctionCallException
{

/**
* Constants
*/

/**
* @type string
*/
const DEFAULT_MESSAGE = 'Invalid delegate';

/**
* The exception code for when a delegate isn't callable
*
* @type int
*/
const CODE_FOR_NON_CALLABLE = 1;

/**
* The message extension for when a delegate isn't callable
*
* @type string
*/
const MESSAGE_EXTENSION_FOR_NON_CALLABLE = ' is unable to be called';

/**
* The message extension format for when a delegate's name is provided
*
* @type string
*/
const MESSAGE_EXTENSION_NAME_FORMAT = ' named `%s`';


/**
* Properties
*/

/**
* @type string
*/
protected $message = self::DEFAULT_MESSAGE;


/**
* Methods
*/

/**
* Create an exception instance for a delegate that isn't callable
*
* @param mixed|null $name The name of the delegate
* @param int $code The exception code
* @param Exception|null $previous A previous exception used for chaining
* @return InvalidDelegateException The newly created exception
*/
public static function forNonCallable($name = null, $code = self::CODE_FOR_NON_CALLABLE, Exception $previous = null)
{
$message = self::DEFAULT_MESSAGE;

if (null !== $name) {
$message .= sprintf(
self::MESSAGE_EXTENSION_NAME_FORMAT,
$name
);
}

$message .= self::MESSAGE_EXTENSION_FOR_NON_CALLABLE;

return new static($message, $code, $previous);
}
}
98 changes: 98 additions & 0 deletions tests/Incoming/Test/Hydrator/AbstractDelegateHydratorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php
/**
* Incoming
*
* @author Trevor Suarez (Rican7)
* @copyright (c) Trevor Suarez
* @link https://github.com/Rican7/incoming
* @license MIT
*/

namespace Incoming\Test\Hydrator;

use DateTime;
use Incoming\Hydrator\AbstractDelegateHydrator;
use Incoming\Structure\Map;
use Incoming\Test\Hydrator\MockDelegateHydrator;
use PHPUnit_Framework_TestCase;

/**
* AbstractDelegateHydratorTest
*/
class AbstractDelegateHydratorTest extends PHPUnit_Framework_TestCase
{

/**
* Helpers
*/

private function getMockDelegateHydrator(callable $delegate)
{
$mock = $this->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());
}
}
Loading

0 comments on commit bbfa8c0

Please sign in to comment.