2014-01-13 110 views
1

我試圖創建一個簡單的自定義Symfony2 dateRange formType輕微扭曲。Symfony2 DateRange自定義類型

這個想法是,用戶可以選擇日期的字符串表示,例如「今天」,「本月」或「自定義」開始/結束日期。

如果用戶選擇某個時間段(例如:「今天」),則提交表單時,忽略開始和結束提交的表單數據,並根據該時間段計算開始和結束時間。

在提交了 「定製」 期間的形式:

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 

在提交了 「明日」 期間的形式(假設該日期是2014年1月1日):

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 

的view/norm數據是一個由句點(int),開始,結束組成的數組。

$viewData = array(
    'period' => 0, 
    'start' => new \DateTime() 
    'end' => new \DateTime() 
); 

模型數據是一個DateRange值對象。

<?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->start = $start; 
     $this->end = $end; 
    } 

    /** 
    * @param DateTime $start 
    * 
    * @return $this 
    */ 
    public function setStart(DateTime $start = null) 
    { 
     $this->start = $start; 

     return $this; 
    } 

    /** 
    * @return DateTime 
    */ 
    public function getStart() 
    { 
     return $this->start; 
    } 

    /** 
    * @param DateTime $end 
    * 
    * @return $this 
    */ 
    public function setEnd(DateTime $end = null) 
    { 
     $this->end = $end; 

     return $this; 
    } 

    /** 
    * @return DateTime 
    */ 
    public function getEnd() 
    { 
     return $this->end; 
    } 
} 

表單類型的樣子:

<?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 
      ->add(
       'period', 
       'choice', 
       array(
        'choices' => array(
         'custom', 
         'today', 
         'tomorrow', 
        ) 
       ) 
      ) 
      ->add('start', 'date') 
      ->add('end', 'date'); 

     $transformer = new DateRangeToArrayTransformer(); 
     $builder->addModelTransformer($transformer); 
    } 

    /** 
    * @param OptionsResolverInterface $resolver 
    */ 
    public function setDefaultOptions(OptionsResolverInterface $resolver) 
    { 
     $resolver->setDefaults(
      array(
       // Don't modify DateRange classes by reference, we treat 
       // them like immutable value objects 
       'by_reference' => false, 
       'error_bubbling' => 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'  => null, 
       'required'   => false 
      ) 
     ); 
    } 

    /** 
    * @return string 
    */ 
    public function getName() 
    { 
     return 'date_range'; 
    } 
} 

最後變壓器的樣子:

<?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' => 0, 
      'start' => $dateRange->getStart(), 
      'end' => $dateRange->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']); 
    } 
} 

我已經決定要標準化的數據存儲爲一個數組,所以我可以保持'時期'價值。

上面的代碼轉換表單數據的預期,但我仍然是基於週期值操縱開始和結束值的問題。

我的第一次嘗試是在變壓器的reverseTransform方法中更改start/end的值。但是,這將break the bijective principal

接下來的嘗試是使用事件。

使用FormEvents:PRE_SUBMIT引入了一些更復雜的問題,即日期格式類型可以作爲原始單個文本或數組提交。使用FormEvents:SUBMIT允許我成功操作表單數據。 $form->getData()返回正確的\ DateRange對象。但是,開始,結束子窗體不會更新(其視圖數據已設置)。

$builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) { 
    $data = $event->getData(); 
    // ... manipulate $data here based on period ... 
    $event->setData($data); 
}); 

所以我的問題:

  • 反正是有操縱父窗體和影響的子窗體的視圖/標準/模型數據?
  • 正在使用事件正確的方式來操縱父數據?
  • 我應該將事件分配給子開始/結束表單而不是父表單嗎?

乾杯Leevi


更新

調整數據變換器來檢查reverseTransform的period檢查期間並返回一個新DateRange還挺工作。

優點:

  • $form->getData();返回正確的DateRange對象

缺點:

  • $form->get('end');返回子形式,但它的數據不反映修改數據reverseTransform設置。
  • 視圖中的開始和結束表單字段不反映不反映修改後的數據集reverseTransform
  • breaks the bijective principal

所有的缺點都是因爲reverseTransform中的變化沒有被壓入子表單中,因爲它們首先被處理。

<?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' => 0, 
      'start' => $dateRange->getStart(), 
      'end' => $dateRange->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); 
    } 
} 

enter image description here enter image description here

回答

1

我肯定會放棄的變壓器和使用自定義的getter/setter方法方法,而不是與驗證,以確保數據的穩定性。

也許我失去了一些東西,迫使你雖然用變壓器...

+0

我使用的變壓器保留在normData的「期間」。 –

+0

如果使用data_class,你不會有這段時間嗎?你不需要那邊的數據轉換器。我認爲處理'period'字段可以通過用戶來完成。 –

0

我應該給孩子開始/結束的形式,而不是父窗體分配的事件?

這聽起來像是一個很好的解決方案,你試過嗎?

$correctDateBasedOnPeriod = function (FormEvent $event) { 
    $period = $event->getForm()->getParent()->get('period')->getData(); 

    if (1 === $period) { 
     $event->setData(new \DateTime('today')); 
    } elseif (2 === $period) { 
     $event->setData(new \DateTime('tomorrow')); 
    } 
}; 

$builder->get('start')->addEventListener(FormEvents::SUBMIT, $correctDateBasedOnPeriod); 
$builder->get('end')->addEventListener(FormEvents::SUBMIT, $correctDateBasedOnPeriod); 

此解決方案有小缺點接着「開始」和「結束」依賴「期間」被他們之前提交。目前,這樣的話 - 只要你加入其他領域前加上「時期」 - 但有沒有保證,這將是在未來相同的(由於潛在的優化)。

但是,如果這個不斷改變,你將最有可能也有一個新的語法來聲明字段之間的依賴關係,使BC行爲。

+0

感謝您的回覆。使用事件允許我根據期間正確設置表單數據。但是,瀏覽器不會根據開始和結束選擇框表單字段中的句點顯示正確的日期。我可以看到這是因爲選擇框(日期,月份)的viewdata已經設置,無法更改。這並不是什麼大事,因爲當這段時間不是「習慣」時,可以隱藏開始和結束。理想情況下,我希望選擇框反映所選範圍,以便用戶在需要時調整範圍。 –

+0

SUBMIT事件的效果顯示在字段本身中(不在子字段中),因此將偵聽器附加到子字段應該有效嗎? –

+0

啊,你說的是帶有選擇框的日期字段。你可以在問題追蹤器上打開一張票嗎?也許我們可以爲此做點什麼。 –