1
votes

I am currently working on a project involving Zend Framework 2 and Doctrine.

I am implementing a Zend\Form and using the DoctrineORMModule\Stdlib\Hydrator\DoctrineEntity to extract and hydrate data from/to the database.

In much of the tutorials I have read, it is ideal to implement the Zend\Stdlib\Hydrator\Strategy\StrategyInterface when you need to convert a specific value before hydrating an object (in my example a date time value string which is an issue when using Doctrine). However, despite my best efforts to implement this, it seems only that the extract() method in my hydration strategy is called, never the hydrate() method. EVER!

To provide a code example, this is what I am doing - I have shortened some aspects of the code for brevity;

// Service

public function getProposerForm() {

    // get required classes from service manager

    $proposerEntity = $this->getServiceManager()->get('tourers_entity_proposer');

    $entityManager = $this->getServiceManager()->get('Doctrine\ORM\EntityManager');

    $formManager = $this->getServiceManager()->get('FormElementManager');
    $proposerFieldset = $formManager->get('tourers_form_proposer_fieldset');
    $proposerForm = $formManager->get('tourers_form_proposal');


    $proposerFieldset->setUseAsBaseFieldset(true);
    $proposerForm->add($proposerFieldset);

    $proposerForm->get('submit')->setValue('Continue');
    $proposerForm->bind($proposerEntity);

    return $proposerForm;
}

.

// Controller

public function proposerAction() {    

    // grab the form from the form service

   $formService = $this->getServiceLocator()->get('tourers_service_forms');

   $form = $formService->getProposerForm();

    if (true === $this->getRequest()->isPost()) {

        $form->setData($this->getRequest()->getPost());

        if (true === $form->isValid()) {

            $proposerEntity = $form->getData();

            $encryptedPolicyId = $formService->saveProposerForm($proposerEntity, $policyId);

            return $this->redirect()->toRoute('tourers/proposal/caravan',array('policyid' => $encryptedPolicyId));


        } else {
            $errors = $form->getMessages();
            var_dump($errors);
        }
    }

    // view

    return new ViewModel(array(
        'form'      =>  $form
        ,'policyid' =>  $policyId
        )
    );

}

.

// Form

class ProposerFieldset extends Fieldset implements InputFilterProviderInterface, ObjectManagerAwareInterface
{

/**
 * @var Doctrine\ORM\EntityManager
 */
private $objectManager;

/**
 * @return Zend\Form\Fieldset
 */

public function init()
{
    // set name

    parent::__construct('Proposer');

    // set the hydrator to the domain object

    $hydrator = new DoctrineEntity($this->objectManager,true);
    $hydrator->addStrategy('proposerDateOfBirth',new DateStrategy);

    $this->setHydrator($hydrator);

    // other form elements below here including proposerDateOfBirth

    $minDate = date('dd\/mm\/yyyy',strtotime('-100 years'));
    $maxDate = date('dd\/mm\/yyyy',strtotime('-16 years'));

    $this->add(array(
        'name'  => 'proposerDateOfBirth'
        ,'type'  => 'Zend\Form\Element\Date'
        ,'attributes' => array(
            'class' => 'form-control'
            ,'id' => 'proposerDateOfBirth'
            ,'placeholder' => 'dd/mm/yyyy'
            ,'min' => $minDate
            ,'max' => $maxDate
            ,'data-date-format' => 'dd/mm/yyyy'

        ),
        'options' => array(
            'label' => 'Date of Birth',
        )
    ));
}
}

.

// Hydrator Strategy

namespace Tourers\Hydrator\Strategy;

use Zend\Stdlib\Hydrator\Strategy\StrategyInterface;

class DateStrategy implements StrategyInterface {

/**
* (non-PHPdoc)
* @see Zend\Stdlib\Hydrator\Strategy.StrategyInterface::extract()
*/
public function extract($value) {
    var_dump($value . ' extracted'); // GETS CALLED
    return $value;

}

/**
 * (non-PHPdoc)
 * @see Zend\Stdlib\Hydrator\Strategy.StrategyInterface::hydrate()
 */
public function hydrate($value) {
    var_dump($value . ' hydrated'); // NEVER CALLED
    return $value;
}
}
1

1 Answers

2
votes

I also discovered this behaviour. As yet I am still lost as to why my custom strategy was not being called.

The Strategies are applied inside the hydrateValue() and extractValue() functions, so its necessary for these functions to be called by the hydrate() and extract() functions in order for Strategies or Custom Strategies to be applied.

My problem was evident in the hydrateByReference() function inside DoctrineModule\Stdlib\Hydrator\DoctrineObject.

It seems that $this->hydrateValue(...) is only called the field has "associations". I do not yet know what these "associations" are, or what I am doing wrong for them to not exist in my data.

When I compared it to the extractByReference() function, I noticed that it always calls $this->extractValue() and does not require any "associations".

In my application, I had already implemented a custom Hydrator class. This means that when I create a form, the Hydrator and Strategies are automatically applied.

  • Inside my Form's init() function, I assign the custom Hydrator.
  • Inside my Custom Hydrator's __construct(), I add the strategies, and Custom Strategies.


So all I needed to do was to override hydrateByReference(...) in my Custom Hydrator to solve the problem. The example is below.

NOTES:

  1. You may also need to override hydrateByValue(...) if you use "by value" hydration.
  2. My solution may break the functionality of "associations".


My Custom Hydrator class:

class maintenance 
        extends DoctrineHydrator
{


    /**
     * Constructor
     *
     * @param ObjectManager $objectManager The ObjectManager to use
     */
    public function __construct($objectManager)
    {
        /*
         * Just call the parent class.
         */
        parent::__construct($objectManager, 
                'Maintenance\Entity\maintenance',  // The FQCN of the hydrated/extracted object
                false                               // If set to true, hydrator will always use entity's public API
            );


        /*
         * Now set up our strategies, and attach them 
         * to the appropriate fields.
         */
        $this->addStrategy('purchasedate', new TIGDateStrategy());
        $this->addStrategy('maintexpirydate', new TIGDateStrategy());

    }

    /**
     * SF Modification, to ensure we call this->hydrateValue on
     * all values before doing anything else with the data.
     * This way, we convert the data first, before trying to
     * store it in a DoctrineEntity.
     * 
     * Hydrate the object using a by-reference logic (this means that values are modified directly without
     * using the public API, in this case setters, and hence override any logic that could be done in those
     * setters)
     *
     * @param  array  $data
     * @param  object $object
     * @return object
     */
    protected function hydrateByReference(array $data, $object)
    {
        $object   = $this->tryConvertArrayToObject($data, $object);
        $metadata = $this->metadata;
        $refl     = $metadata->getReflectionClass();

        foreach ($data as $field => $value) {
            // Ignore unknown fields
            if (!$refl->hasProperty($field)) {
                continue;
            }

            // SF Mod
            $value = $this->hydrateValue($field, $value);
            // End SF Mod

            $value        = $this->handleTypeConversions($value, $metadata->getTypeOfField($field));
            $reflProperty = $refl->getProperty($field);
            $reflProperty->setAccessible(true);

            if ($metadata->hasAssociation($field)) {
                $target = $metadata->getAssociationTargetClass($field);

                if ($metadata->isSingleValuedAssociation($field)) {
                    $value = $this->toOne($target, $this->hydrateValue($field, $value));
                    $reflProperty->setValue($object, $value);
                } elseif ($metadata->isCollectionValuedAssociation($field)) {
                    $this->toMany($object, $field, $target, $value);
                }
            } else {
                $reflProperty->setValue($object, $value);
            }
        }

        return $object;
    }

}