title: Symfony2 DateRange custom type
body: <p>I'm trying to create a simple custom Symfony2 dateRange formType with a slight twist.</p>
  
  <p>The idea is that the user can choose a string representation of the date eg "today", "this month" or "custom" start / end dates.</p>
  
  <p>When the form is submitted if a user chooses a period eg: "today" the start and end submitted form data are ignored and the start and end are calculated based on the period.</p>
  
  <p>When submitting the form with "custom" period:</p>
  
  <pre><code>When I fill in the form  
     And submit "period" with "custom"  
     And submit "start" with "2014-01-01"  
     And submit "end" with "2014-01-01"  
  Then the form should display:  
      "custom" in the period select box  
      And "2014-01-01" in start  
      And "2014-01-01" in end
  </code></pre>
  
  <p>When submitting the form with "tomorrow" period (assuming the date is 2014-01-01):</p>
  
  <pre><code>When I fill in the form  
     And submit "period" with "tomorrow"  
  Then the form should display:  
      "tomorrow" in the period select box  
      And "2014-01-02" in start  
      And "2014-01-02" in end
  </code></pre>
  
  <p>The view / norm data is an array consisting of a period (int), start, end.</p>
  
  <pre><code>$viewData = array(
      'period' =&gt; 0,
      'start' =&gt;  new \DateTime()
      'end' =&gt;  new \DateTime()
  );
  </code></pre>
  
  <p>The model data is a DateRange value object.</p>
  
  <pre><code>&lt;?php
  
  namespace Nsm\Bundle\ApiBundle\Form\Model;
  
  use DateTime;
  
  class DateRange
  {
      /**
       * @var DateTime
       */
      protected $start;
  
      /**
       * @var DateTime
       */
      protected $end;
  
      /**
       * @param DateTime $start
       * @param DateTime $end
       */
      public function __construct(DateTime $start = null, DateTime $end = null)
      {
          $this-&gt;start = $start;
          $this-&gt;end   = $end;
      }
  
      /**
       * @param DateTime $start
       *
       * @return $this
       */
      public function setStart(DateTime $start = null)
      {
          $this-&gt;start = $start;
  
          return $this;
      }
  
      /**
       * @return DateTime
       */
      public function getStart()
      {
          return $this-&gt;start;
      }
  
      /**
       * @param DateTime $end
       *
       * @return $this
       */
      public function setEnd(DateTime $end = null)
      {
          $this-&gt;end = $end;
  
          return $this;
      }
  
      /**
       * @return DateTime
       */
      public function getEnd()
      {
          return $this-&gt;end;
      }
  }
  </code></pre>
  
  <p>The form type looks like:</p>
  
  <pre><code>&lt;?php
  
  namespace Nsm\Bundle\ApiBundle\Form\Type;
  
  use Nsm\Bundle\ApiBundle\Form\DataTransformer\DateRangeToArrayTransformer;
  use Symfony\Component\Form\AbstractType;
  use Symfony\Component\Form\FormBuilderInterface;
  use Symfony\Component\Form\FormEvent;
  use Symfony\Component\Form\FormEvents;
  use Symfony\Component\OptionsResolver\Options;
  use Symfony\Component\OptionsResolver\OptionsResolver;
  use Symfony\Component\OptionsResolver\OptionsResolverInterface;
  use Symfony\Component\Form\Form;
  
  class DateRangeType extends AbstractType
  {
      /**
       * @param FormBuilderInterface $builder
       * @param array                $options
       */
      public function buildForm(FormBuilderInterface $builder, array $options)
      {
          $builder
              -&gt;add(
                  'period',
                  'choice',
                  array(
                      'choices' =&gt; array(
                          'custom',
                          'today',
                          'tomorrow',
                      )
                  )
              )
              -&gt;add('start', 'date')
              -&gt;add('end', 'date');
  
          $transformer = new DateRangeToArrayTransformer();
          $builder-&gt;addModelTransformer($transformer);
      }
  
      /**
       * @param OptionsResolverInterface $resolver
       */
      public function setDefaultOptions(OptionsResolverInterface $resolver)
      {
          $resolver-&gt;setDefaults(
              array(
                  // Don't modify DateRange classes by reference, we treat
                  // them like immutable value objects
                  'by_reference'   =&gt; false,
                  'error_bubbling' =&gt; false,
                  // If initialized with a DateRange object, FormType initializes
                  // this option to "DateRange". Since the internal, normalized
                  // representation is not DateRange, but an array, we need to unset
                  // this option.
                  'data_class'        =&gt; null,
                  'required'          =&gt; false
              )
          );
      }
  
      /**
       * @return string
       */
      public function getName()
      {
          return 'date_range';
      }
  }
  </code></pre>
  
  <p>And finally the transformer looks like:</p>
  
  <pre><code>&lt;?php
  
  namespace Nsm\Bundle\ApiBundle\Form\DataTransformer;
  
  use Nsm\Bundle\ApiBundle\Form\Model\DateRange;
  use Symfony\Component\Form\DataTransformerInterface;
  use Symfony\Component\Form\Exception\UnexpectedTypeException;
  use Symfony\Component\OptionsResolver\OptionsResolverInterface;
  
  class DateRangeToArrayTransformer implements DataTransformerInterface
  {
      /**
       * Model to Norm
       *
       * @param mixed $dateRange
       *
       * @return array|mixed|string
       * @throws \Symfony\Component\Form\Exception\UnexpectedTypeException
       */
      public function transform($dateRange)
      {
          if (null === $dateRange) {
              return null;
          }
  
          if (!$dateRange instanceof DateRange) {
              throw new UnexpectedTypeException($dateRange, 'DateRange');
          }
  
          return array(
              'period' =&gt; 0,
              'start' =&gt; $dateRange-&gt;getStart(),
              'end' =&gt; $dateRange-&gt;getEnd()
          );
      }
  
      /**
       * Norm to Model
       *
       * @param $value
       *
       * @return DateRange|null
       * @throws \Symfony\Component\Form\Exception\UnexpectedTypeException
       */
      public function reverseTransform($value)
      {
          if (null === $value) {
              return null;
          }
  
          if (!is_array($value)) {
              throw new UnexpectedTypeException($value, 'array');
          }
  
          // Check here if period is custom and calculate dates
          return new DateRange($value['start'], $value['end']);
      }
  }
  </code></pre>
  
  <p>I've decided to store the normalised data as an array so I can keep the 'period' value.</p>
  
  <p>The code above transforms the form data as expected but I still ave an issue of manipulating the start and end values based on the period value.</p>
  
  <p>My first attempt was to change the value of start / end in the reverseTransform method of the transformer. However this would <a href="https://www.youtube.com/watch?v=JAX13g5orwo&amp;feature=youtu.be&amp;t=16m" rel="nofollow noreferrer">break the bijective principal</a>.</p>
  
  <p>Next attempt was to use events. </p>
  
  <p>Using <code>FormEvents:PRE_SUBMIT</code> introduced a few more complications namely the date form type can be submitted as a raw single text or array.</p>
  
  <p>Using <code>FormEvents:SUBMIT</code> allows me to manipulate the form data successfully. <code>$form-&gt;getData()</code> returns the correct \DateRange object. However the start, end child forms are not updated (their view data is already set).</p>
  
  <pre><code>$builder-&gt;addEventListener(FormEvents::SUBMIT, function (FormEvent $event) {
      $data = $event-&gt;getData();
      // ... manipulate $data here based on period ...
      $event-&gt;setData($data);
  });
  </code></pre>
  
  <p>So my question:</p>
  
  <ul>
  <li>Is there anyway to manipulate a parent form and affect the view / norm / model data of child forms?</li>
  <li>Is using events the correct way to manipulate the parent data?</li>
  <li>Should I be assigning events to the child start / end forms rather than the parent form?</li>
  </ul>
  
  <p>Cheers Leevi</p>
  
  <hr>
  
  <p><strong>Update</strong></p>
  
  <p>Tweaking the data transformer to check the <code>period</code> in reverseTransform to check the period and return a new <code>DateRange</code> kinda works.</p>
  
  <p>Pros:</p>
  
  <ul>
  <li><code>$form-&gt;getData();</code> returns the correct <code>DateRange</code> object</li>
  </ul>
  
  <p>Cons:</p>
  
  <ul>
  <li><code>$form-&gt;get('end');</code> returns the child form but it's data does not reflect the modified data set in reverseTransform.</li>
  <li>start and end form fields in the view do not reflect not reflect the modified data set in reverseTransform</li>
  <li><a href="https://www.youtube.com/watch?v=JAX13g5orwo&amp;feature=youtu.be&amp;t=16m" rel="nofollow noreferrer">breaks the bijective principal</a>.</li>
  </ul>
  
  <p>All of the cons relate to the fact that the changes in reverseTransform are not pushed down into the child forms as they are processed first.</p>
  
  <pre><code>&lt;?php
  
  namespace Nsm\Bundle\ApiBundle\Form\DataTransformer;
  
  use Nsm\Bundle\ApiBundle\Form\Model\DateRange;
  use Symfony\Component\Form\DataTransformerInterface;
  use Symfony\Component\Form\Exception\UnexpectedTypeException;
  use Symfony\Component\OptionsResolver\OptionsResolverInterface;
  
  class DateRangeToArrayTransformer implements DataTransformerInterface
  {
      /**
       * Model to Norm
       *
       * @param mixed $dateRange
       *
       * @return array|mixed|string
       * @throws \Symfony\Component\Form\Exception\UnexpectedTypeException
       */
      public function transform($dateRange)
      {
          if (null === $dateRange) {
              return null;
          }
  
          if (!$dateRange instanceof DateRange) {
              throw new UnexpectedTypeException($dateRange, 'DateRange');
          }
  
          return array(
              'period' =&gt; 0,
              'start' =&gt; $dateRange-&gt;getStart(),
              'end' =&gt; $dateRange-&gt;getEnd()
          );
      }
  
      /**
       * Norm to Model
       *
       * @param $value
       *
       * @return DateRange|null
       * @throws \Symfony\Component\Form\Exception\UnexpectedTypeException
       */
      public function reverseTransform($value)
      {
          if (null === $value) {
              return null;
          }
  
          if (!is_array($value)) {
              throw new UnexpectedTypeException($value, 'array');
          }
  
          switch($value['period']) {
              // Custom
              case 0:
                  $start = $value['start'];
                  $end = $value['end'];
                  break;
              // Today
              case 1:
                  $start = new \DateTime('today');
                  $end = new \DateTime('today');
                  break;
              // Tomorrow
              case 2:
                  $start = new \DateTime('tomorrow');
                  $end = new \DateTime('tomorrow');
                  break;
              // This week
              case 3:
                  $start = new \DateTime('this week');
                  $end = new \DateTime('this week');
                  break;
              default:
                  break;
          }
  
          // Check here if period is custom and calculate dates
          return new DateRange($start, $end);
      }
  }
  </code></pre>
  
  <p><img src="https://i.stack.imgur.com/iKUBk.png" alt="enter image description here">
  <img src="https://i.stack.imgur.com/nC3JU.png" alt="enter image description here"></p>