Symfony2 DateRange自定义类型

时间:2014-01-13 07:16:13

标签: php symfony symfony-forms

我正在尝试创建一个简单的自定义Symfony2 dateRange formType,稍加扭曲。

这个想法是用户可以选择日期的字符串表示,例如“今天”,“本月”或“自定义”开始/结束日期。

如果用户选择了一个句点,例如:“今天”,则提交表单时,将忽略开始和结束提交的表单数据,并根据期间计算开始和结束。

提交带有“自定义”期限的表单时:

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-01-01):

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),start,end组成的数组。

$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);
});

所以我的问题:

  • 无论如何操纵父表单并影响子表单的视图/规范/模型数据?
  • 使用事件是否正确操作父数据?
  • 我应该将事件分配给子开始/结束表单而不是父表单吗?

Cheers 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

2 个答案:

答案 0 :(得分:1)

我肯定会放弃变压器并使用自定义getter / setters方法而不是验证来确保数据的稳定性。

也许我错过了一些迫使你使用变形金刚的东西......

答案 1 :(得分: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行为。