Skip to content

Commit bbfa8c0

Browse files
committed
Merge branch 'develop'. Prepare release v0.2.0
2 parents 6e8f2d8 + 02b982b commit bbfa8c0

File tree

8 files changed

+408
-1
lines changed

8 files changed

+408
-1
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# CHANGELOG
2+
3+
## 0.2.0
4+
5+
### Features
6+
7+
- Introduced a new `AbstractDelegateHydrator` class to allow for implementing a hydrator while using a delegate callback
8+
- While this facilitates simple method delegation, its real design was to allow for the use of type-hinted hydrators
9+
that could circumvent PHP's type-system limitations.
10+
- For more info, read the class doc-block of the new `AbstractDelegateHydrator` class

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
all: install test lint checkstyle
1+
all: install test lint check-style
22

33
install:
44
composer install --prefer-dist

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,36 @@ $user = $incoming->process(
147147
// ...
148148
```
149149

150+
Missing type hints? PHP's type-system's restrictions can be circumvented!:
151+
152+
```php
153+
class UserHydrator extends Incoming\Hydrator\AbstractDelegateHydrator
154+
{
155+
// Boom! Type-hintable arguments!
156+
// (For more info, see the `AbstractDelegateHydrator` class doc-block)
157+
public function hydrateModel(Incoming\Structure\Map $input, User $model)
158+
{
159+
$model->setName($input['name']);
160+
// ...
161+
162+
return $model;
163+
}
164+
}
165+
166+
// Create our incoming processor
167+
$incoming = new Incoming\Processor();
168+
169+
// Process our raw form/request input into a User model
170+
$user = $incoming->process(
171+
$_POST, // Our HTTP form-data array
172+
new User(), // Our model to hydrate
173+
new UserHydrator() // The hydrator above
174+
);
175+
176+
// Validate and save the user
177+
// ...
178+
```
179+
150180

151181
## Wait, what? Why not just use "x" or "y"?
152182

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
/**
3+
* Incoming
4+
*
5+
* @author Trevor Suarez (Rican7)
6+
* @copyright (c) Trevor Suarez
7+
* @link https://github.com/Rican7/incoming
8+
* @license MIT
9+
*/
10+
11+
namespace Incoming\Hydrator;
12+
13+
use Incoming\Hydrator\Exception\InvalidDelegateException;
14+
15+
/**
16+
* AbstractDelegateHydrator
17+
*
18+
* An abstract hydrator that allows for the hydration to be delegated to another
19+
* callable. By default, a named method is attempted to be found, but any
20+
* callable could be returned through overrides.
21+
*
22+
* This enables a lot of interesting uses, most notably this allows hydrators to
23+
* be created that have strongly type-hinted hydration arguments while still
24+
* perfectly satisfying the `HydratorInterface`. Essentially this allows the
25+
* bypassing of the type variance rules enforced by PHP in a way that provides a
26+
* generics-like definition. Ultimately, if/when PHP gets generics this will no
27+
* longer be necessary, as one could simply implement a hydrator using typed
28+
* arguments like: `HydratorInterface<IncomingDataType, ModelType>`
29+
*
30+
* @link http://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)
31+
* @link http://en.wikipedia.org/wiki/Generic_programming
32+
*/
33+
abstract class AbstractDelegateHydrator implements HydratorInterface
34+
{
35+
36+
/**
37+
* Constants
38+
*/
39+
40+
/**
41+
* The name of the default delegate method
42+
*
43+
* @type string
44+
*/
45+
const DEFAULT_DELEGATE_METHOD_NAME = 'hydrateModel';
46+
47+
48+
/**
49+
* Methods
50+
*/
51+
52+
/**
53+
* {@inheritdoc}
54+
*
55+
* @param mixed $incoming The input data
56+
* @param mixed $model The model to hydrate
57+
* @return mixed The hydrated model
58+
*/
59+
public function hydrate($incoming, $model)
60+
{
61+
return call_user_func(
62+
$this->getDelegate(),
63+
$incoming,
64+
$model
65+
);
66+
}
67+
68+
/**
69+
* Get the delegate hydration callable
70+
*
71+
* Override this method if a custom delegate is desired
72+
*
73+
* @return callable The delegate hydrator callable
74+
*/
75+
protected function getDelegate()
76+
{
77+
$delegate = [$this, static::DEFAULT_DELEGATE_METHOD_NAME];
78+
79+
if (!is_callable($delegate, false, $callable_name)) {
80+
throw InvalidDelegateException::forNonCallable($callable_name);
81+
}
82+
83+
return $delegate;
84+
}
85+
86+
/**
87+
* The delegate hydrate method
88+
*
89+
* This doc-block and commented out abstract method is provided here to show
90+
* what the delegate method signature WOULD be if PHP allowed the proper
91+
* typing support to enable a generic definition in this manner
92+
*
93+
* See the class description for more info
94+
*
95+
* @param IncomingDataType $incoming The input data
96+
* @param ModelType $model The model to hydrate
97+
* @return ModelType The hydrated model
98+
*/
99+
// abstract protected function hydrateModel(IncomingDataType $incoming, ModelType $model);
100+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
/**
3+
* Incoming
4+
*
5+
* @author Trevor Suarez (Rican7)
6+
* @copyright (c) Trevor Suarez
7+
* @link https://github.com/Rican7/incoming
8+
* @license MIT
9+
*/
10+
11+
namespace Incoming\Hydrator\Exception;
12+
13+
use BadFunctionCallException;
14+
use Exception;
15+
16+
/**
17+
* InvalidDelegateException
18+
*
19+
* An exception to be thrown when an invalid delegate method, function, or
20+
* callback is provided to a caller
21+
*/
22+
class InvalidDelegateException extends BadFunctionCallException
23+
{
24+
25+
/**
26+
* Constants
27+
*/
28+
29+
/**
30+
* @type string
31+
*/
32+
const DEFAULT_MESSAGE = 'Invalid delegate';
33+
34+
/**
35+
* The exception code for when a delegate isn't callable
36+
*
37+
* @type int
38+
*/
39+
const CODE_FOR_NON_CALLABLE = 1;
40+
41+
/**
42+
* The message extension for when a delegate isn't callable
43+
*
44+
* @type string
45+
*/
46+
const MESSAGE_EXTENSION_FOR_NON_CALLABLE = ' is unable to be called';
47+
48+
/**
49+
* The message extension format for when a delegate's name is provided
50+
*
51+
* @type string
52+
*/
53+
const MESSAGE_EXTENSION_NAME_FORMAT = ' named `%s`';
54+
55+
56+
/**
57+
* Properties
58+
*/
59+
60+
/**
61+
* @type string
62+
*/
63+
protected $message = self::DEFAULT_MESSAGE;
64+
65+
66+
/**
67+
* Methods
68+
*/
69+
70+
/**
71+
* Create an exception instance for a delegate that isn't callable
72+
*
73+
* @param mixed|null $name The name of the delegate
74+
* @param int $code The exception code
75+
* @param Exception|null $previous A previous exception used for chaining
76+
* @return InvalidDelegateException The newly created exception
77+
*/
78+
public static function forNonCallable($name = null, $code = self::CODE_FOR_NON_CALLABLE, Exception $previous = null)
79+
{
80+
$message = self::DEFAULT_MESSAGE;
81+
82+
if (null !== $name) {
83+
$message .= sprintf(
84+
self::MESSAGE_EXTENSION_NAME_FORMAT,
85+
$name
86+
);
87+
}
88+
89+
$message .= self::MESSAGE_EXTENSION_FOR_NON_CALLABLE;
90+
91+
return new static($message, $code, $previous);
92+
}
93+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
/**
3+
* Incoming
4+
*
5+
* @author Trevor Suarez (Rican7)
6+
* @copyright (c) Trevor Suarez
7+
* @link https://github.com/Rican7/incoming
8+
* @license MIT
9+
*/
10+
11+
namespace Incoming\Test\Hydrator;
12+
13+
use DateTime;
14+
use Incoming\Hydrator\AbstractDelegateHydrator;
15+
use Incoming\Structure\Map;
16+
use Incoming\Test\Hydrator\MockDelegateHydrator;
17+
use PHPUnit_Framework_TestCase;
18+
19+
/**
20+
* AbstractDelegateHydratorTest
21+
*/
22+
class AbstractDelegateHydratorTest extends PHPUnit_Framework_TestCase
23+
{
24+
25+
/**
26+
* Helpers
27+
*/
28+
29+
private function getMockDelegateHydrator(callable $delegate)
30+
{
31+
$mock = $this->getMockBuilder('Incoming\Hydrator\AbstractDelegateHydrator')
32+
->setMethods([AbstractDelegateHydrator::DEFAULT_DELEGATE_METHOD_NAME])
33+
->getMock();
34+
35+
$mock->expects($this->any())
36+
->method(AbstractDelegateHydrator::DEFAULT_DELEGATE_METHOD_NAME)
37+
->will($this->returnCallback($delegate));
38+
39+
return $mock;
40+
}
41+
42+
43+
/**
44+
* Tests
45+
*/
46+
47+
public function testHydrate()
48+
{
49+
$test_input_data = Map::fromArray([
50+
'year' => 1983,
51+
'month' => 1,
52+
'day' => 2,
53+
]);
54+
$test_model = new DateTime();
55+
56+
$test_delegate_callable = function (Map $incoming, DateTime $model) {
57+
$model->setDate(
58+
$incoming->get('year'),
59+
$incoming->get('month'),
60+
$incoming->get('day')
61+
);
62+
63+
return $model;
64+
};
65+
66+
$test_hydrator = $this->getMockDelegateHydrator($test_delegate_callable);
67+
68+
$hydrated = $test_hydrator->hydrate($test_input_data, $test_model);
69+
70+
$this->assertEquals($test_model, $hydrated);
71+
$this->assertSame($test_input_data['year'], (int) $hydrated->format('Y'));
72+
$this->assertSame($test_input_data['month'], (int) $hydrated->format('m'));
73+
$this->assertSame($test_input_data['day'], (int) $hydrated->format('j'));
74+
}
75+
76+
/**
77+
* @expectedException Incoming\Hydrator\Exception\InvalidDelegateException
78+
*/
79+
public function testHydrateWithNonCallableThrowsException()
80+
{
81+
$mock_hydrator = new MockDelegateHydrator();
82+
83+
$mock_hydrator->hydrate([], new DateTime());
84+
}
85+
86+
/**
87+
* @expectedException PHPUnit_Framework_Error
88+
*/
89+
public function testHydrateWithImproperTypesCausesTypeError()
90+
{
91+
$test_delegate_callable = function (Map $incoming, DateTime $model) {
92+
};
93+
94+
$test_hydrator = $this->getMockDelegateHydrator($test_delegate_callable);
95+
96+
$test_hydrator->hydrate([], new DateTime());
97+
}
98+
}

0 commit comments

Comments
 (0)