Skip to content
This repository

Hydration Strategies #2072

Merged
merged 17 commits into from over 1 year ago

5 participants

Walter Tamboer Don't Add Me To Your Organization a.k.a The Travis Bot David Windell Matthew Weier O'Phinney Ron
Walter Tamboer

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];
    }
}
Walter Tamboer

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

Don't Add Me To Your Organization a.k.a The Travis Bot

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

David Windell

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

Walter Tamboer

@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.

Matthew Weier O'Phinney
Owner

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.

Walter Tamboer

@weierophinney Is this what you mean?

Matthew Weier O'Phinney
Owner

@WalterTamboer yes, perfect! :)

Don't Add Me To Your Organization a.k.a The Travis Bot

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

Don't Add Me To Your Organization a.k.a The Travis Bot

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

Don't Add Me To Your Organization a.k.a The Travis Bot

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

Matthew Weier O'Phinney weierophinney referenced this pull request from a commit August 01, 2012
Matthew Weier O'Phinney [#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
Matthew Weier O'Phinney weierophinney referenced this pull request from a commit August 01, 2012
Matthew Weier O'Phinney [#2072] CS fixes
- trailing whitespace
f4edd63
Matthew Weier O'Phinney weierophinney referenced this pull request from a commit August 01, 2012
Matthew Weier O'Phinney [#2072] Updated README.md
- Added new feature to changelog
038ed12
Matthew Weier O'Phinney weierophinney merged commit 83a9021 into from August 01, 2012
Matthew Weier O'Phinney weierophinney closed this August 01, 2012
Matthew Weier O'Phinney
Owner

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.

Ron

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);
Deleted user Unknown referenced this pull request from a commit August 01, 2012
Matthew Weier O'Phinney [#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
Deleted user Unknown referenced this pull request from a commit August 01, 2012
Matthew Weier O'Phinney [#2072] CS fixes
- trailing whitespace
c73d5f2
Deleted user Unknown referenced this pull request from a commit August 01, 2012
Matthew Weier O'Phinney [#2072] Updated README.md
- Added new feature to changelog
ba017c5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
117  library/Zend/Stdlib/Hydrator/AbstractHydrator.php
... ...
@@ -0,0 +1,117 @@
  1
+<?php
  2
+/**
  3
+ * Zend Framework (http://framework.zend.com/)
  4
+ *
  5
+ * @link      http://github.com/zendframework/zf2 for the canonical source repository
  6
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
  7
+ * @license   http://framework.zend.com/license/new-bsd New BSD License
  8
+ * @package   Zend_Stdlib
  9
+ */
  10
+
  11
+namespace Zend\Stdlib\Hydrator;
  12
+
  13
+use ArrayObject;
  14
+use Zend\Stdlib\Hydrator\StrategyEnabledInterface;
  15
+use Zend\Stdlib\Hydrator\Strategy\StrategyInterface;
  16
+
  17
+/**
  18
+ * @category   Zend
  19
+ * @package    Zend_Stdlib
  20
+ * @subpackage Hydrator
  21
+ */
  22
+abstract class AbstractHydrator implements HydratorInterface, StrategyEnabledInterface
  23
+{
  24
+    /**
  25
+     * The list with strategies that this hydrator has.
  26
+     * 
  27
+     * @var ArrayObject
  28
+     */
  29
+    protected $strategies;
  30
+
  31
+    /**
  32
+     * Initializes a new instance of this class. 
  33
+     */
  34
+    public function __construct()
  35
+    {
  36
+        $this->strategies = new ArrayObject();
  37
+    }
  38
+    
  39
+    /**
  40
+     * Gets the strategy with the given name.
  41
+     * 
  42
+     * @param string $name The name of the strategy to get.
  43
+     * @return StrategyInterface
  44
+     */
  45
+    public function getStrategy($name)
  46
+    {
  47
+        return $this->strategies[$name];
  48
+    }
  49
+    
  50
+    /**
  51
+     * Checks if the strategy with the given name exists.
  52
+     * 
  53
+     * @param string $name The name of the strategy to check for.
  54
+     * @return bool
  55
+     */
  56
+    public function hasStrategy($name)
  57
+    {
  58
+        return array_key_exists($name, $this->strategies);
  59
+    }
  60
+    
  61
+    /**
  62
+     * Adds the given strategy under the given name.
  63
+     * 
  64
+     * @param string $name The name of the strategy to register.
  65
+     * @param StrategyInterface $strategy The strategy to register.
  66
+     * @return HydratorInterface
  67
+     */
  68
+    public function addStrategy($name, StrategyInterface $strategy)
  69
+    {
  70
+        $this->strategies[$name] = $strategy;
  71
+        return $this;
  72
+    }
  73
+    
  74
+    /**
  75
+     * Removes the strategy with the given name.
  76
+     * 
  77
+     * @param string $name The name of the strategy to remove.
  78
+     * @return HydratorInterface
  79
+     */
  80
+    public function removeStrategy($name)
  81
+    {
  82
+        unset($this->strategies[$name]);
  83
+        return $this;
  84
+    }
  85
+    
  86
+    /**
  87
+     * Converts a value for extraction. If no strategy exists the plain value is returned.
  88
+     * 
  89
+     * @param string $name The name of the strategy to use.
  90
+     * @param mixed $value The value that should be converted.
  91
+     * @return mixed
  92
+     */
  93
+    public function extractValue($name, $value)
  94
+    {
  95
+        if ($this->hasStrategy($name)) {
  96
+            $strategy = $this->getStrategy($name);
  97
+            $value = $strategy->extract($value);
  98
+        }
  99
+        return $value;
  100
+    }
  101
+    
  102
+    /**
  103
+     * Converts a value for hydration. If no strategy exists the plain value is returned.
  104
+     * 
  105
+     * @param string $name The name of the strategy to use.
  106
+     * @param mixed $value The value that should be converted.
  107
+     * @return mixed
  108
+     */
  109
+    public function hydrateValue($name, $value)
  110
+    {
  111
+        if ($this->hasStrategy($name)) {
  112
+            $strategy = $this->getStrategy($name);
  113
+            $value = $strategy->hydrate($value);
  114
+        }
  115
+        return $value;
  116
+    }
  117
+}
20  library/Zend/Stdlib/Hydrator/ArraySerializable.php
@@ -17,8 +17,9 @@
17 17
  * @package    Zend_Stdlib
18 18
  * @subpackage Hydrator
19 19
  */
20  
-class ArraySerializable implements HydratorInterface
  20
+class ArraySerializable extends AbstractHydrator
21 21
 {
  22
+
22 23
     /**
23 24
      * Extract values from the provided object
24 25
      *
@@ -32,11 +33,15 @@ public function extract($object)
32 33
     {
33 34
         if (!is_callable(array($object, 'getArrayCopy'))) {
34 35
             throw new Exception\BadMethodCallException(sprintf(
35  
-                '%s expects the provided object to implement getArrayCopy()',
36  
-                __METHOD__
  36
+                '%s expects the provided object to implement getArrayCopy()', __METHOD__
37 37
             ));
38 38
         }
39  
-        return $object->getArrayCopy();
  39
+
  40
+        $data = $object->getArrayCopy();
  41
+        array_walk($data, function(&$value, $name) {
  42
+            $value = $this->extractValue($name, $value);
  43
+        });
  44
+        return $data;
40 45
     }
41 46
 
42 47
     /**
@@ -52,14 +57,17 @@ public function extract($object)
52 57
      */
53 58
     public function hydrate(array $data, $object)
54 59
     {
  60
+        array_walk($data, function(&$value, $name) {
  61
+            $value = $this->hydrateValue($name, $value);
  62
+        });
  63
+
55 64
         if (is_callable(array($object, 'exchangeArray'))) {
56 65
             $object->exchangeArray($data);
57 66
         } elseif (is_callable(array($object, 'populate'))) {
58 67
             $object->populate($data);
59 68
         } else {
60 69
             throw new Exception\BadMethodCallException(sprintf(
61  
-                '%s expects the provided object to implement exchangeArray() or populate()',
62  
-                __METHOD__
  70
+                '%s expects the provided object to implement exchangeArray() or populate()', __METHOD__
63 71
             ));
64 72
         }
65 73
         return $object;
20  library/Zend/Stdlib/Hydrator/ClassMethods.php
@@ -17,7 +17,7 @@
17 17
  * @package    Zend_Stdlib
18 18
  * @subpackage Hydrator
19 19
  */
20  
-class ClassMethods implements HydratorInterface
  20
+class ClassMethods extends AbstractHydrator
21 21
 {
22 22
     /**
23 23
      * Flag defining whether array keys are underscore-separated (true) or camel case (false)
@@ -31,6 +31,7 @@ class ClassMethods implements HydratorInterface
31 31
      */
32 32
     public function __construct($underscoreSeparatedKeys = true)
33 33
     {
  34
+        parent::__construct();
34 35
         $this->underscoreSeparatedKeys = $underscoreSeparatedKeys;
35 36
     }
36 37
 
@@ -47,8 +48,7 @@ public function extract($object)
47 48
     {
48 49
         if (!is_object($object)) {
49 50
             throw new Exception\BadMethodCallException(sprintf(
50  
-                '%s expects the provided $object to be a PHP object)',
51  
-                __METHOD__
  51
+                '%s expects the provided $object to be a PHP object)', __METHOD__
52 52
             ));
53 53
         }
54 54
 
@@ -58,11 +58,11 @@ public function extract($object)
58 58
         };
59 59
         $attributes = array();
60 60
         $methods = get_class_methods($object);
61  
-        foreach($methods as $method) {
62  
-            if(preg_match('/^get[A-Z]\w*/', $method)) {
  61
+        foreach ($methods as $method) {
  62
+            if (preg_match('/^get[A-Z]\w*/', $method)) {
63 63
                 // setter verification
64 64
                 $setter = preg_replace('/^get/', 'set', $method);
65  
-                if(!in_array($setter, $methods)) {
  65
+                if (!in_array($setter, $methods)) {
66 66
                     continue;
67 67
                 }
68 68
                 $attribute = substr($method, 3);
@@ -70,8 +70,7 @@ public function extract($object)
70 70
                 if ($this->underscoreSeparatedKeys) {
71 71
                     $attribute = preg_replace_callback('/([A-Z])/', $transform, $attribute);
72 72
                 }
73  
-
74  
-                $attributes[$attribute] = $object->$method();
  73
+                $attributes[$attribute] = $this->extractValue($attribute, $object->$method());
75 74
             }
76 75
         }
77 76
 
@@ -92,8 +91,7 @@ public function hydrate(array $data, $object)
92 91
     {
93 92
         if (!is_object($object)) {
94 93
             throw new Exception\BadMethodCallException(sprintf(
95  
-                '%s expects the provided $object to be a PHP object)',
96  
-                __METHOD__
  94
+                '%s expects the provided $object to be a PHP object)', __METHOD__
97 95
             ));
98 96
         }
99 97
 
@@ -108,6 +106,8 @@ public function hydrate(array $data, $object)
108 106
             }
109 107
             $method = 'set' . ucfirst($property);
110 108
             if (method_exists($object, $method)) {
  109
+                $value = $this->hydrateValue($property, $value);
  110
+
111 111
                 $object->$method($value);
112 112
             }
113 113
         }
16  library/Zend/Stdlib/Hydrator/ObjectProperty.php
@@ -17,7 +17,7 @@
17 17
  * @package    Zend_Stdlib
18 18
  * @subpackage Hydrator
19 19
  */
20  
-class ObjectProperty implements HydratorInterface
  20
+class ObjectProperty extends AbstractHydrator
21 21
 {
22 22
     /**
23 23
      * Extract values from an object
@@ -32,12 +32,15 @@ public function extract($object)
32 32
     {
33 33
         if (!is_object($object)) {
34 34
             throw new Exception\BadMethodCallException(sprintf(
35  
-                '%s expects the provided $object to be a PHP object)',
36  
-                __METHOD__
  35
+                '%s expects the provided $object to be a PHP object)', __METHOD__
37 36
             ));
38 37
         }
39 38
 
40  
-        return get_object_vars($object);
  39
+        $data = get_object_vars($object);
  40
+        array_walk($data, function(&$value, $name) {
  41
+            $value = $this->extractValue($name, $value);
  42
+        });
  43
+        return $data;
41 44
     }
42 45
 
43 46
     /**
@@ -54,12 +57,11 @@ public function hydrate(array $data, $object)
54 57
     {
55 58
         if (!is_object($object)) {
56 59
             throw new Exception\BadMethodCallException(sprintf(
57  
-                '%s expects the provided $object to be a PHP object)',
58  
-                __METHOD__
  60
+                '%s expects the provided $object to be a PHP object)', __METHOD__
59 61
             ));
60 62
         }
61 63
         foreach ($data as $property => $value) {
62  
-            $object->$property = $value;
  64
+            $object->$property = $this->hydrateValue($property, $value);
63 65
         }
64 66
         return $object;
65 67
     }
9  library/Zend/Stdlib/Hydrator/Reflection.php
@@ -14,7 +14,7 @@
14 14
 use Zend\Stdlib\Exception;
15 15
 use Zend\Stdlib\Hydrator\HydratorInterface;
16 16
 
17  
-class Reflection implements HydratorInterface
  17
+class Reflection extends AbstractHydrator
18 18
 {
19 19
     /**
20 20
      * Simple in-memory array cache of ReflectionProperties used.
@@ -32,7 +32,10 @@ public function extract($object)
32 32
     {
33 33
         $result = array();
34 34
         foreach(self::getReflProperties($object) as $property) {
35  
-            $result[$property->getName()] = $property->getValue($object);
  35
+            $propertyName = $property->getName();
  36
+            
  37
+            $value = $property->getValue($object);
  38
+            $result[$propertyName] = $this->extractValue($propertyName, $value);
36 39
         }
37 40
 
38 41
         return $result;
@@ -50,7 +53,7 @@ public function hydrate(array $data, $object)
50 53
         $reflProperties = self::getReflProperties($object);
51 54
         foreach($data as $key => $value) {
52 55
             if (isset($reflProperties[$key])) {
53  
-                $reflProperties[$key]->setValue($object, $value);
  56
+                $reflProperties[$key]->setValue($object, $this->hydrateValue($key, $value));
54 57
             }
55 58
         }
56 59
         return $object;
41  library/Zend/Stdlib/Hydrator/Strategy/DefaultStrategy.php
... ...
@@ -0,0 +1,41 @@
  1
+<?php
  2
+/**
  3
+ * Zend Framework (http://framework.zend.com/)
  4
+ *
  5
+ * @link      http://github.com/zendframework/zf2 for the canonical source repository
  6
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
  7
+ * @license   http://framework.zend.com/license/new-bsd New BSD License
  8
+ * @package   Zend_Stdlib
  9
+ */
  10
+
  11
+namespace Zend\Stdlib\Hydrator\Strategy;
  12
+
  13
+/**
  14
+ * @category   Zend
  15
+ * @package    Zend_Stdlib
  16
+ * @subpackage Hydrator
  17
+ */
  18
+class DefaultStrategy implements StrategyInterface
  19
+{
  20
+    /**
  21
+     * Converts the given value so that it can be extracted by the hydrator.
  22
+     * 
  23
+     * @param mixed $value The original value.
  24
+     * @return mixed Returns the value that should be extracted.
  25
+     */
  26
+    public function extract($value)
  27
+    {
  28
+        return $value;
  29
+    }
  30
+
  31
+    /**
  32
+     * Converts the given value so that it can be hydrated by the hydrator.
  33
+     * 
  34
+     * @param mixed $value The original value.
  35
+     * @return mixed Returns the value that should be hydrated.
  36
+     */
  37
+    public function hydrate($value)
  38
+    {
  39
+        return $value;
  40
+    }
  41
+}
35  library/Zend/Stdlib/Hydrator/Strategy/StrategyInterface.php
... ...
@@ -0,0 +1,35 @@
  1
+<?php
  2
+/**
  3
+ * Zend Framework (http://framework.zend.com/)
  4
+ *
  5
+ * @link      http://github.com/zendframework/zf2 for the canonical source repository
  6
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
  7
+ * @license   http://framework.zend.com/license/new-bsd New BSD License
  8
+ * @package   Zend_Stdlib
  9
+ */
  10
+
  11
+namespace Zend\Stdlib\Hydrator\Strategy;
  12
+
  13
+/**
  14
+ * @category   Zend
  15
+ * @package    Zend_Stdlib
  16
+ * @subpackage Hydrator
  17
+ */
  18
+interface StrategyInterface
  19
+{
  20
+    /**
  21
+     * Converts the given value so that it can be extracted by the hydrator.
  22
+     * 
  23
+     * @param mixed $value The original value.
  24
+     * @return mixed Returns the value that should be extracted.
  25
+     */
  26
+    public function extract($value);
  27
+
  28
+    /**
  29
+     * Converts the given value so that it can be hydrated by the hydrator.
  30
+     * 
  31
+     * @param mixed $value The original value.
  32
+     * @return mixed Returns the value that should be hydrated.
  33
+     */
  34
+    public function hydrate($value);
  35
+}
54  library/Zend/Stdlib/Hydrator/StrategyEnabledInterface.php
... ...
@@ -0,0 +1,54 @@
  1
+<?php
  2
+/**
  3
+ * Zend Framework (http://framework.zend.com/)
  4
+ *
  5
+ * @link      http://github.com/zendframework/zf2 for the canonical source repository
  6
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
  7
+ * @license   http://framework.zend.com/license/new-bsd New BSD License
  8
+ * @package   Zend_Stdlib
  9
+ */
  10
+
  11
+namespace Zend\Stdlib\Hydrator;
  12
+
  13
+use Zend\Stdlib\Hydrator\Strategy\StrategyInterface;
  14
+
  15
+/**
  16
+ * @category   Zend
  17
+ * @package    Zend_Stdlib
  18
+ * @subpackage Hydrator
  19
+ */
  20
+interface StrategyEnabledInterface
  21
+{
  22
+    /**
  23
+     * Adds the given strategy under the given name.
  24
+     * 
  25
+     * @param string $name The name of the strategy to register.
  26
+     * @param StrategyInterface $strategy The strategy to register.
  27
+     * @return HydratorInterface
  28
+     */
  29
+    public function addStrategy($name, StrategyInterface $strategy);
  30
+
  31
+    /**
  32
+     * Gets the strategy with the given name.
  33
+     * 
  34
+     * @param string $name The name of the strategy to get.
  35
+     * @return StrategyInterface
  36
+     */
  37
+    public function getStrategy($name);
  38
+
  39
+    /**
  40
+     * Checks if the strategy with the given name exists.
  41
+     * 
  42
+     * @param string $name The name of the strategy to check for.
  43
+     * @return bool
  44
+     */
  45
+    public function hasStrategy($name);
  46
+
  47
+    /**
  48
+     * Removes the strategy with the given name.
  49
+     * 
  50
+     * @param string $name The name of the strategy to remove.
  51
+     * @return HydratorInterface
  52
+     */
  53
+    public function removeStrategy($name);
  54
+}
39  tests/Zend/Form/FormTest.php
@@ -63,6 +63,11 @@ public function getOneToManyEntity()
63 63
         return $product;
64 64
     }
65 65
 
  66
+    public function populateHydratorStrategyForm()
  67
+    {
  68
+        $this->form->add(new Element('entities'));
  69
+    }
  70
+
66 71
     public function populateForm()
67 72
     {
68 73
         $this->form->add(new Element('foo'));
@@ -917,4 +922,38 @@ public function testRemoveCollectionFromValidationGroupWhenZeroCountAndNoData()
917 922
         $this->form->setData($dataWithoutCollection);
918 923
         $this->assertTrue($this->form->isValid());
919 924
     }
  925
+
  926
+    public function testExtractDataHydratorStrategy()
  927
+    {
  928
+        $this->populateHydratorStrategyForm();
  929
+        
  930
+        $hydrator = new Hydrator\ObjectProperty();
  931
+        $hydrator->addStrategy('entities', new TestAsset\HydratorStrategy());
  932
+        $this->form->setHydrator($hydrator);
  933
+        
  934
+        $model = new TestAsset\HydratorStrategyEntityA();
  935
+        $this->form->bind($model);
  936
+        
  937
+        $validSet = array(
  938
+            'entities' => array(
  939
+                111, 
  940
+                333
  941
+            ),
  942
+        );
  943
+        
  944
+        $this->form->setData($validSet);
  945
+        $this->form->isValid();
  946
+        
  947
+        $data = $this->form->getData(Form::VALUES_AS_ARRAY);
  948
+        $this->assertEquals($validSet, $data);
  949
+        
  950
+        $entities = $model->getEntities();
  951
+        $this->assertCount(2, $entities);
  952
+        
  953
+        $this->assertEquals(111, $entities[0]->getField1());
  954
+        $this->assertEquals(333, $entities[1]->getField1());
  955
+        
  956
+        $this->assertEquals('AAA', $entities[0]->getField2());
  957
+        $this->assertEquals('CCC', $entities[1]->getField2());
  958
+    }
920 959
 }
64  tests/Zend/Form/TestAsset/HydratorStrategy.php
... ...
@@ -0,0 +1,64 @@
  1
+<?php
  2
+/**
  3
+ * Zend Framework (http://framework.zend.com/)
  4
+ *
  5
+ * @link      http://github.com/zendframework/zf2 for the canonical source repository
  6
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
  7
+ * @license   http://framework.zend.com/license/new-bsd New BSD License
  8
+ * @package   Zend_Form
  9
+ */
  10
+
  11
+namespace ZendTest\Form\TestAsset;
  12
+
  13
+use Zend\Stdlib\Hydrator\Strategy\DefaultStrategy;
  14
+
  15
+class HydratorStrategy extends DefaultStrategy
  16
+{
  17
+    /**
  18
+     * A simulated storage device which is just an array with Car objects.
  19
+     * 
  20
+     * @var array
  21
+     */
  22
+    private $simulatedStorageDevice;
  23
+    
  24
+    public function __construct()
  25
+    {
  26
+        $this->simulatedStorageDevice = array();
  27
+        $this->simulatedStorageDevice[] = new HydratorStrategyEntityB(111, 'AAA');
  28
+        $this->simulatedStorageDevice[] = new HydratorStrategyEntityB(222, 'BBB');
  29
+        $this->simulatedStorageDevice[] = new HydratorStrategyEntityB(333, 'CCC');
  30
+    }
  31
+    
  32
+    public function extract($value)
  33
+    {
  34
+        $result = array();
  35
+        foreach ($value as $instance) {
  36
+            $result[] = $instance->getField1();
  37
+        }
  38
+        return $result;
  39
+    }
  40
+    
  41
+    public function hydrate($value)
  42
+    {
  43
+        $result = $value;
  44
+        if (is_array($value)) {
  45
+            $result = array();
  46
+            foreach ($value as $field1) {
  47
+                $result[] = $this->findEntity($field1);
  48
+            }
  49
+        }
  50
+        return $result;
  51
+    }
  52
+    
  53
+    private function findEntity($field1)
  54
+    {
  55
+        $result = null;
  56
+        foreach ($this->simulatedStorageDevice as $entity) {
  57
+            if ($entity->getField1() == $field1) {
  58
+                $result = $entity;
  59
+                break;
  60
+            }
  61
+        }
  62
+        return $result;
  63
+    }
  64
+}
75  tests/Zend/Form/TestAsset/HydratorStrategyEntityA.php
... ...
@@ -0,0 +1,75 @@
  1
+<?php
  2
+/**
  3
+ * Zend Framework (http://framework.zend.com/)
  4
+ *
  5
+ * @link      http://github.com/zendframework/zf2 for the canonical source repository
  6
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
  7
+ * @license   http://framework.zend.com/license/new-bsd New BSD License
  8
+ * @package   Zend_Form
  9
+ */
  10
+
  11
+namespace ZendTest\Form\TestAsset;
  12
+
  13
+use Zend\InputFilter\Input;
  14
+use Zend\InputFilter\InputFilter As RealInputFilter;
  15
+use Zend\InputFilter\InputFilterInterface;
  16
+use Zend\InputFilter\InputFilterAwareInterface;
  17
+
  18
+class HydratorStrategyEntityA implements InputFilterAwareInterface
  19
+{
  20
+    public $entities; // public to make testing easier!
  21
+    private $inputFilter; // used to test forms
  22
+    
  23
+    public function __construct()
  24
+    {
  25
+        $this->entities = array();
  26
+    }
  27
+
  28
+    public function addEntity(HydratorStrategyEntityB $entity)
  29
+    {
  30
+        $this->entities[] = $entity;
  31
+    }
  32
+
  33
+    public function getEntities()
  34
+    {
  35
+        return $this->entities;
  36
+    }
  37
+
  38
+    public function setEntities($entities)
  39
+    {
  40
+        $this->entities = $entities;
  41
+    }
  42
+
  43
+    public function getInputFilter()
  44
+    {
  45
+        if (!$this->inputFilter) {
  46
+            $input = new Input();
  47
+            $input->setName('entities');
  48
+            $input->setRequired(false);
  49
+            
  50
+            $this->inputFilter = new RealInputFilter();
  51
+            $this->inputFilter->add($input);
  52
+        }
  53
+
  54
+        return $this->inputFilter;
  55
+    }
  56
+    
  57
+    public function setInputFilter(InputFilterInterface $inputFilter)
  58
+    {
  59
+        $this->inputFilter = $inputFilter;
  60
+    }
  61
+
  62
+    // Add the getArrayCopy method so we can test the ArraySerializable hydrator:
  63
+    public function getArrayCopy()
  64
+    {
  65
+        return get_object_vars($this);
  66
+    }
  67
+
  68
+    // Add the populate method so we can test the ArraySerializable hydrator:
  69
+    public function populate($data)
  70
+    {
  71
+        foreach ($data as $name => $value) {
  72
+            $this->$name = $value;
  73
+        }
  74
+    }
  75
+}
47  tests/Zend/Form/TestAsset/HydratorStrategyEntityB.php
... ...
@@ -0,0 +1,47 @@
  1
+<?php
  2
+/**
  3
+ * Zend Framework (http://framework.zend.com/)
  4
+ *
  5
+ * @link      http://github.com/zendframework/zf2 for the canonical source repository
  6
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
  7
+ * @license   http://framework.zend.com/license/new-bsd New BSD License
  8
+ * @package   Zend_Form
  9
+ */
  10
+
  11
+namespace ZendTest\Form\TestAsset;
  12
+
  13
+use Zend\InputFilter\InputFilterAwareInterface;
  14
+
  15
+class HydratorStrategyEntityB
  16
+{
  17
+    private $field1;
  18
+    private $field2;
  19
+    
  20
+    public function __construct($field1, $field2)
  21
+    {
  22
+        $this->field1 = $field1;
  23
+        $this->field2 = $field2;
  24
+    }
  25
+    
  26
+    public function getField1()
  27
+    {
  28
+        return $this->field1;
  29
+    }
  30
+    
  31
+    public function getField2()
  32
+    {
  33
+        return $this->field2;
  34
+    }
  35
+    
  36
+    public function setField1($value)
  37
+    {
  38
+        $this->field1 = $value;
  39
+        return $this;
  40
+    }
  41
+    
  42
+    public function setField2($value)
  43
+    {
  44
+        $this->field2 = $value;
  45
+        return $this;
  46
+    }
  47
+}
109  tests/Zend/Stdlib/HydratorStrategyTest.php
... ...
@@ -0,0 +1,109 @@
  1
+<?php
  2
+/**
  3
+ * Zend Framework (http://framework.zend.com/)
  4
+ *
  5
+ * @link      http://github.com/zendframework/zf2 for the canonical source repository
  6
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
  7
+ * @license   http://framework.zend.com/license/new-bsd New BSD License
  8
+ * @package   Zend_Stdlib
  9
+ */
  10
+
  11
+namespace ZendTest\Stdlib;
  12
+
  13
+use Zend\Stdlib\Hydrator\HydratorInterface;
  14
+use Zend\Stdlib\Hydrator\ClassMethods;
  15
+use ZendTest\StdLib\TestAsset\HydratorStrategy;
  16
+use ZendTest\StdLib\TestAsset\HydratorStrategyEntityA;
  17
+use ZendTest\StdLib\TestAsset\HydratorStrategyEntityB;
  18
+
  19
+/**
  20
+ * @category   Zend
  21
+ * @package    Zend_Stdlib
  22
+ * @subpackage UnitTests
  23
+ * @group      Zend_Stdlib
  24
+ */
  25
+class HydratorTest extends \PHPUnit_Framework_TestCase
  26
+{
  27
+    /**
  28
+     * The hydrator that is used during testing.
  29
+     * 
  30
+     * @var HydratorInterface
  31
+     */
  32
+    private $hydrator;
  33
+    
  34
+    public function setUp()
  35
+    {
  36
+        $this->hydrator = new ClassMethods();
  37
+    }
  38
+
  39
+    public function testAddingStrategy()
  40
+    {
  41
+        $this->assertAttributeCount(0, 'strategies', $this->hydrator);
  42
+        
  43
+        $this->hydrator->addStrategy('myStrategy', new HydratorStrategy());
  44
+        
  45
+        $this->assertAttributeCount(1, 'strategies', $this->hydrator);
  46
+    }
  47
+
  48
+    public function testCheckStrategyEmpty()
  49
+    {
  50
+        $this->assertFalse($this->hydrator->hasStrategy('myStrategy'));
  51
+    }
  52
+
  53
+    public function testCheckStrategyNotEmpty()
  54
+    {
  55
+        $this->hydrator->addStrategy('myStrategy', new HydratorStrategy());
  56
+        
  57
+        $this->assertTrue($this->hydrator->hasStrategy('myStrategy'));
  58
+    }
  59
+
  60
+    public function testRemovingStrategy()
  61
+    {
  62
+        $this->assertAttributeCount(0, 'strategies', $this->hydrator);
  63
+        
  64
+        $this->hydrator->addStrategy('myStrategy', new HydratorStrategy());
  65
+        $this->assertAttributeCount(1, 'strategies', $this->hydrator);
  66
+        
  67
+        $this->hydrator->removeStrategy('myStrategy');
  68
+        $this->assertAttributeCount(0, 'strategies', $this->hydrator);
  69
+    }
  70
+
  71
+    public function testRetrieveStrategy()
  72
+    {
  73
+        $strategy = new HydratorStrategy();
  74
+        $this->hydrator->addStrategy('myStrategy', $strategy);
  75
+        
  76
+        $this->assertEquals($strategy, $this->hydrator->getStrategy('myStrategy'));
  77
+    }
  78
+
  79
+    public function testExtractingObjects()
  80
+    {
  81
+        $this->hydrator->addStrategy('entities', new HydratorStrategy());
  82
+        
  83
+        $entityA = new HydratorStrategyEntityA();
  84
+        $entityA->addEntity(new HydratorStrategyEntityB(111, 'AAA'));
  85
+        $entityA->addEntity(new HydratorStrategyEntityB(222, 'BBB'));
  86
+        
  87
+        $attributes = $this->hydrator->extract($entityA);
  88
+        
  89
+        $this->assertContains(111, $attributes['entities']);
  90
+        $this->assertContains(222, $attributes['entities']);
  91
+    }
  92
+
  93
+    public function testHydratingObjects()
  94
+    {
  95
+        $this->hydrator->addStrategy('entities', new HydratorStrategy());
  96
+        
  97
+        $entityA = new HydratorStrategyEntityA();
  98
+        $entityA->addEntity(new HydratorStrategyEntityB(111, 'AAA'));
  99
+        $entityA->addEntity(new HydratorStrategyEntityB(222, 'BBB'));
  100
+        
  101
+        $attributes = $this->hydrator->extract($entityA);
  102
+        $attributes['entities'][] = 333;
  103
+        
  104
+        $this->hydrator->hydrate($attributes, $entityA);
  105
+        $entities = $entityA->getEntities();
  106
+        
  107
+        $this->assertCount(3, $entities);
  108
+    }
  109
+}
64  tests/Zend/Stdlib/TestAsset/HydratorStrategy.php
... ...
@@ -0,0 +1,64 @@
  1
+<?php
  2
+/**
  3
+ * Zend Framework (http://framework.zend.com/)
  4
+ *
  5
+ * @link      http://github.com/zendframework/zf2 for the canonical source repository
  6
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
  7
+ * @license   http://framework.zend.com/license/new-bsd New BSD License
  8
+ * @package   Zend_Stdlib
  9
+ */
  10
+
  11
+namespace ZendTest\Stdlib\TestAsset;
  12
+
  13
+use Zend\Stdlib\Hydrator\Strategy\DefaultStrategy;
  14
+
  15
+class HydratorStrategy extends DefaultStrategy
  16
+{
  17
+    /**
  18
+     * A simulated storage device which is just an array with Car objects.
  19
+     * 
  20
+     * @var array
  21
+     */
  22
+    private $simulatedStorageDevice;
  23
+    
  24
+    public function __construct()
  25
+    {
  26
+        $this->simulatedStorageDevice = array();
  27
+        $this->simulatedStorageDevice[] = new HydratorStrategyEntityB(111, 'AAA');
  28
+        $this->simulatedStorageDevice[] = new HydratorStrategyEntityB(222, 'BBB');
  29
+        $this->simulatedStorageDevice[] = new HydratorStrategyEntityB(333, 'CCC');
  30
+    }
  31
+    
  32
+    public function extract($value)
  33
+    {
  34
+        $result = array();
  35
+        foreach ($value as $instance) {
  36
+            $result[] = $instance->getField1();
  37
+        }
  38
+        return $result;
  39
+    }
  40
+    
  41
+    public function hydrate($value)
  42
+    {
  43
+        $result = $value;
  44
+        if (is_array($value)) {
  45
+            $result = array();
  46
+            foreach ($value as $field1) {
  47
+                $result[] = $this->findEntity($field1);
  48
+            }
  49
+        }
  50
+        return $result;
  51
+    }
  52
+    
  53
+    private function findEntity($field1)
  54
+    {
  55
+        $result = null;
  56
+        foreach ($this->simulatedStorageDevice as $entity) {
  57
+            if ($entity->getField1() == $field1) {
  58
+                $result = $entity;
  59
+                break;
  60
+            }
  61
+        }
  62
+        return $result;
  63
+    }
  64
+}
75  tests/Zend/Stdlib/TestAsset/HydratorStrategyEntityA.php
... ...
@@ -0,0 +1,75 @@
  1
+<?php
  2
+/**
  3
+ * Zend Framework (http://framework.zend.com/)
  4
+ *
  5
+ * @link      http://github.com/zendframework/zf2 for the canonical source repository
  6
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
  7
+ * @license   http://framework.zend.com/license/new-bsd New BSD License
  8
+ * @package   Zend_Stdlib
  9
+ */
  10
+
  11
+namespace ZendTest\Stdlib\TestAsset;
  12
+
  13
+use Zend\InputFilter\Input;
  14
+use Zend\InputFilter\InputFilter;
  15
+use Zend\InputFilter\InputFilterInterface;
  16
+use Zend\InputFilter\InputFilterAwareInterface;
  17
+
  18
+class HydratorStrategyEntityA implements InputFilterAwareInterface
  19
+{
  20
+    public $entities; // public to make testing easier!
  21
+    private $inputFilter; // used to test forms
  22
+    
  23
+    public function __construct()
  24
+    {
  25
+        $this->entities = array();
  26
+    }
  27
+
  28
+    public function addEntity(HydratorStrategyEntityB $entity)
  29
+    {
  30
+        $this->entities[] = $entity;
  31
+    }
  32
+
  33
+    public function getEntities()
  34
+    {
  35
+        return $this->entities;
  36
+    }
  37
+
  38
+    public function setEntities($entities)
  39
+    {
  40
+        $this->entities = $entities;
  41
+    }
  42
+
  43
+    public function getInputFilter()
  44
+    {
  45
+        if (!$this->inputFilter) {
  46
+            $input = new Input();
  47
+            $input->setName('entities');