Hydration Strategies #2072

Merged
merged 17 commits into from Aug 1, 2012

Projects

None yet

5 participants

@waltertamboer

The hydration component is very useful, especially in combination with forms. There is a small problem with it though. The hydrator only tells you how hydration takes places but not what gets hydrated. Normally this is not a problem but when one uses forms there will be trouble.

Take for example a Garage object that is filled with Car objects. Now one binds an instance of Garage to a form. In this form is a <select> element present in which multiple options can be selected (thus <select name="cars[]" multiple="multiple">). One probably creates the element as follow:

$element = new Select();
$element->setAttribute('multiple', 'multiple');
$element->setAttribute('size', '10');
$element->setAttribute('options', $dataProvider->getEntities());
$element->setName('cars');
$this->add($element);

Binding looks as folow:

$garage = new Garage();
$garage->addCar(new Car(1, 'Ford')); // id, name
$garage->addCar(new Car(3, 'Fiat')); // id, name

$form = new GarageForm();
$form->initElements();
$form->setHydrator(new ClassMethods());
$form->setInputFilter($garage->getInputFilter());

// Bind the garage object which makes the form populate its elements:
$form->bind($garage);

All good so far untill the hydrator starts to extract cars from the garage. The garage has a method getCars that returns a list with Car objects. There also is a method setCars that expects a list with car objects.

Since a list with car objects is returned, the Select element doesn't know how to deal with it and will thus error (for example like this: Notice: Object of class Application\Entity\Car could not be converted to int in Zend\Form\View\Helper\FormSelect.php on line 155)

The second problem is that when the form is posted, a list with integers is posted. This isbecause the options in the select element look like this:

<option value="1">Ford</option>
<option value="2">Mercedes</option>
<option value="3">Fiat</option>

So how can the hydrator populate the bound object with a list of cars again? It's not possible unless one writes a custom hydrator that does that for you. An example of that is currently present in DoctrineModule.
The problem with this approach is that one needs a custom hydrator for each combination of object types that is returned.

This PR provides a solution for this problem. Instead of creating custom hydrators you can now create hydration strategies. These are reusable throughout the application. A hydration strategy basically acts as a filter before extracting and hydrating takes place.

In the garage example all one has to do is register a strategy to extract and return cars:

$hydrator->addStrategy('cars', new CarsHydrationStrategy());

This CarsHydrationStrategy class can look like this for example:

class CarsHydrationStrategy extends DefaultStrategy
{
    private $simulatedStorageDevice;

    public function __construct()
    {
        $this->simulatedStorageDevice = array();
        $this->simulatedStorageDevice[1] = new Car(1, 'Ford');
        $this->simulatedStorageDevice[2] = new Car(2, 'Mercedes');
        $this->simulatedStorageDevice[3] = new Car(3, 'Fiat');
    }

    public function extract($value)
    {
        $result = array();
        foreach ($value as $instance) {
            $result[] = $instance->getId();
        }
        return $result;
    }

    public function hydrate($value)
    {
        $result = $value;
        if (is_array($value)) {
            $result = array();
            foreach ($value as $id) {
                $result[] = $this->findEntity($id);
            }
        }
        return $result;
    }

    private function findEntity($id)
    {
        return $this->simulatedStorageDevice[$id];
    }
}
@waltertamboer

I still need to write tests but I think it's wise to discuss this PR first.

@travisbot

This pull request fails (merged 8b14081 into 59929bf).

@davidwindell

@WalterTamboer could this not be handled by creating custom Hydrators for each of your strategies that extend one of the standard hydrators?

@waltertamboer

@davidwindell That would be possible but than you would still be writing a hydrator per form. It can also occur that objects are fetched from different places. Your car brands could come from a database while your address list could come from a web service for example. In my opinion this is the cleanest way to provide this functionality in such a way that it's re-usable for other forms.

@weierophinney
Zend Framework member

I'd split this up a bit. You've defined a strategy interface, which is good. I'd argue that we should have a StrategyEnabledInterface which incorporates the add/get/has/removeStrategy() methods, and then have hydrators opt-in to that; that way hydrators don't need to have strategy awareness if the hierarchy is flat. We could certainly have the AbstractHydrator implement both the HydratorInterface and the StrategyEnabledInterface, and have the default hydrators extend it -- but if somebody wants to create a super-simple hydrator, they wouldn't need to do anything more than implement the basic HydratorInterface.

Otherwise, looks quite sane.

@waltertamboer

@weierophinney Is this what you mean?

@weierophinney
Zend Framework member

@WalterTamboer yes, perfect! :)

@travisbot

This pull request fails (merged 84f0f3f into 59929bf).

@travisbot

This pull request fails (merged b1eb26b into 59929bf).

@travisbot

This pull request fails (merged 04a8c10 into 59929bf).

@weierophinney weierophinney added a commit that referenced this pull request Aug 1, 2012
@weierophinney weierophinney [#2072] Fix for 5.3
- Defined a number of closures that relied on the current method context.
  Unfortunately, this does not work in 5.3 unless you import the current object
  ($self = $this; use ($self) in the closure declaration).
- Renamed HydratorStrategyTest to remove naming conflicts
fa2290c
@weierophinney weierophinney added a commit that referenced this pull request Aug 1, 2012
@weierophinney weierophinney [#2072] CS fixes
- trailing whitespace
f4edd63
@weierophinney weierophinney added a commit that referenced this pull request Aug 1, 2012
@weierophinney weierophinney [#2072] Updated README.md
- Added new feature to changelog
038ed12
@weierophinney weierophinney merged commit 83a9021 into zendframework:master Aug 1, 2012
@weierophinney
Zend Framework member

There were a number of PHP 5.3-related issues (surrounding closures), CS issues, and issues with the stdlib tests (conflicting test name, bad import of test assets). I corrected all these when merging.

Thanks -- this will be a really nice feature, and simplify a lot of use cases.

@GitRon

I tried to build a custom strategy like it is shown here but couldn't get it to work. My constructor is called but not the two functions extract and hydrate.
I added the strategy as follows:

        $hydrator = new DoctrineEntity($entityManager);
        $hydrator->getHydrator()->addStrategy('my_attribute', new MyHydrationStrategy());
        $form->setHydrator($hydrator);
@ghost Unknown pushed a commit that referenced this pull request Jul 14, 2013
@weierophinney weierophinney [#2072] Fix for 5.3
- Defined a number of closures that relied on the current method context.
  Unfortunately, this does not work in 5.3 unless you import the current object
  ($self = $this; use ($self) in the closure declaration).
- Renamed HydratorStrategyTest to remove naming conflicts
32a495f
@ghost Unknown pushed a commit that referenced this pull request Jul 14, 2013
@weierophinney weierophinney [#2072] CS fixes
- trailing whitespace
c73d5f2
@ghost Unknown pushed a commit that referenced this pull request Jul 14, 2013
@weierophinney weierophinney [#2072] Updated README.md
- Added new feature to changelog
ba017c5
@weierophinney weierophinney added a commit to zendframework/zend-stdlib that referenced this pull request May 15, 2015
@weierophinney weierophinney [zendframework/zendframework#2072] Fix for 5.3
- Defined a number of closures that relied on the current method context.
  Unfortunately, this does not work in 5.3 unless you import the current object
  ($self = $this; use ($self) in the closure declaration).
- Renamed HydratorStrategyTest to remove naming conflicts
7bdd8b8
@weierophinney weierophinney added a commit to zendframework/zend-stdlib that referenced this pull request May 15, 2015
@weierophinney weierophinney [zendframework/zendframework#2072] CS fixes
- trailing whitespace
a22bdcb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment