在 FormExtension 中获取实体的初始值

Get initial value of entity in FormExtension

在我的更新表单中,我想在包含实体初始值的输入上添加一个数据属性。这样,当用户修改它时,我将能够突出显示它。

最后只会高亮显示用户修改的输入。

我只想在更新中使用它,而不是在创建中使用它。

为此,我创建了一个表单扩展,如下所示:

class IFormTypeExtension extends AbstractTypeExtension
{
...

public static function getExtendedTypes()
{
    //I want to be able to extend any form type
    return [FormType::class];
}

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults([
        'is_iform' => false,
        'is_iform_modification' => function (Options $options) {
            return $options['is_iform'] ? null : false;
        },
    ]);
    $resolver->setAllowedTypes('is_iform', 'bool');
    $resolver->setAllowedTypes('is_iform_modification', ['bool', 'null']);
}

public function buildView(FormView $view, FormInterface $form, array $options)
{
    if (!$options['is_iform'] && !$this->isParentIForm($form)) {
        return;
    }

    //We need to add the original value in the input as data-attributes
    if (is_string($form->getViewData()) || is_int($form->getViewData())) {
        $originValue = $form->getViewData();
    } elseif (is_array($form->getViewData())) {
        if (is_object($form->getNormData())) {
            $originValue = implode('###', array_keys($form->getViewData()));
        } elseif (is_array($form->getNormData()) && count($form->getNormData()) > 0 && is_object($form->getNormData()[0])) {
            $originValue = implode('###', array_keys($form->getViewData()));
        } else {
            $originValue = implode('###', $form->getViewData());
        }
    } else {
        //There's no value yet
        $originValue = '';
    }

    $view->vars['attr'] = array_merge($view->vars['attr'], ['data-orig-value' => $originValue]);
}

private function isParentIForm(FormInterface $form)
{
    if (null === $form->getParent()) {
        return $form->getConfig()->getOption('is_iform');
    }

    return $this->isParentIForm($form->getParent());
}
}

正如您在 buildView 方法中看到的那样,我从 ViewData 中获取了 originValue。

在很多情况下,这很有效。

但是如果我的表单中有任何验证错误或者如果我通过 AJAX 重新加载我的表单,ViewData 包含新信息而不是我要更新的实体的值。

如何获取原始实体的值?

  1. 我不想在这里进行数据库请求。
  2. 我想我可以使用 FormEvents::POST_SET_DATA 事件,然后在会话中保存实体值并在 buildView 中使用它们。
  3. 我也可以在我的 OptionResolver 中提供一个新的选项来请求初始实体。
  4. 是否可以让实体的原始数据直接形成buildView? (如果我没记错的话,这就是我们调用 handleRequest 方法之前的表单)。

有人想要一个带有控制器的例子。我不认为它真的很有趣,因为有了 FormExtension,代码会自动添加。但无论如何,这是我在控制器中创建表单的方法:

$form = $this->createForm(CustomerType::class, $customer)->handleRequest($request);

并且在 CustomerType 中,我将使用 configureOptions() 添加 'is_iform' 键:

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults([
        "translation_domain" => "customer",
        "data_class" => Customer::class,
        'is_iform' => true //This line will activate the extension
    ]);
}

这可能是一个自以为是的答案。也可能有更好的方法。 我不太喜欢你的表单扩展,因为它真的很复杂,不清楚发生了什么,至少在我看来是这样。

我的建议:当表单提交发生时,在您的控制器中您应该执行以下操作

// ((*)) maybe store customer, see below
$form = $this->createForm(CustomerType::class, $customer);
$form->handleRequest($request);

if($form->isSubmitted() && $form->isValid()) {
   // easy case, you got this.
   $em->flush();

   return $this->redirect(); // or another response

} elseif($form->isSubmitted()) {

   // form was submitted with errors, have to refresh entity!

   // REFRESH - see discussion below for alternatives
   $em->refresh($customer);

   // then create form again with original values:
   $form = $this->createForm(CustomerType::class, $customer); 
}
// other stuff
return $this->render(..., ['form' => $form->createView(), ...]);

因此,从本质上讲,当表单验证失败时,您刷新实体并重新创建表单,从而避免了实体状态更改的问题。我相信这种方法最终比破解表单更容易神奇地不更新值或重新设置旧值。

现在的问题是:如何刷新实体?最简单的方法:从数据库重新加载:

$em->refresh($customer); // easiest approach, will likely run another query.

备选方案:

  1. 您创建一个包含相同值但在更改时不会自动更改原始对象的客户 DTO,而不是向表单提供 $customer。如果表单验证失败,你可以重新生成DTO。

  2. 而不是 refresh($customer),这很可能会 运行 另一个查询(除了可能没有,如果你有缓存),你可以通过 DefaultCacheEntityHydrator, you would have to create your own EntityCacheKey 对象(不是真的很难),生成一个缓存条目(DefaultCacheEntityHydrator::buildCacheEntry() 在上面 ((*)) )并在需要恢复时恢复条目。免责声明:我不知道 if/how 这适用于集合(即实体可能具有的集合属性)。

话虽这么说...如果您真的出于某种原因真的想要一个表单扩展,您可能想要使用 PRE_SET_DATA 处理程序来形成事件,该处理程序将数据存储在表单类型对象中,然后在 buildView 上使用这些值。我不会在会话中存储一些东西,因为我看不出有必要......你对数据库查询的厌恶令人困惑,如果这是你所有恶作剧的主要原因

最后,我设法让它发挥作用,但我并不完全相信我所做的。

无法从表单中获取原始数据或添加新的 属性(表单在表单扩展中是只读的)。

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder->addEventListener(
        FormEvents::POST_SET_DATA,
        function (FormEvent $event) {
            $form = $event->getForm();
            if ('_token' === $form->getName()) {
                return;
            }

            $data = $event->getData();
            $this->session->set('iform_'.$form->getName(), is_object($data) ? clone $data : $data);
        }
    );
}

我在这里所做的只是在会话中按名称注册表单值。 如果它是一个对象,我需要克隆它,因为表单会在稍后的过程中修改它,我想使用表单的原始状态。

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults([
        'is_iform' => false,
        'is_iform_modification' => function (Options $options) {
            return $options['is_iform'] ? null : false;
        },
    ]);
    $resolver->setAllowedTypes('is_iform', 'bool');
    $resolver->setAllowedTypes('is_iform_modification', ['bool', 'null']);
}

配置选项没有改变。 然后,根据值类型,我创建 "data-orig-value" :

public function buildView(FormView $view, FormInterface $form, array $options)
{
    if (!$options['is_iform'] && !$this->isParentIForm($form)) {
        return;
    }

    $propertyValue = $this->session->get('iform_'.$form->getName());
    $originValue = '';

    try {
        if (null !== $propertyValue) {
            //We need to add the original value in the input as data-attributes
            if (is_bool($propertyValue)) {
                $originValue = $propertyValue ? 1 : 0;
            } elseif (is_string($propertyValue) || is_int($propertyValue)) {
                $originValue = $propertyValue;
            } elseif (is_array($propertyValue) || $propertyValue instanceof Collection) {
                if (is_object($propertyValue)) {
                    $originValue = implode('###', array_map(function ($object) {
                        return $object->getId();
                    }, $propertyValue->toArray()));
                } elseif (is_array($propertyValue) && count($propertyValue) > 0 && is_object(array_values($propertyValue)[0])) {
                    $originValue = implode('###', array_map(function ($object) {
                        return $object->getId();
                    }, $propertyValue));
                } else {
                    $originValue = implode('###', $propertyValue);
                }
            } elseif ($propertyValue instanceof DateTimeInterface) {
                $originValue = \IntlDateFormatter::formatObject($propertyValue, $form->getConfig()->getOption('format', 'dd/mm/yyyy'));
            } elseif (is_object($propertyValue)) {
                $originValue = $propertyValue->getId();
            } else {
                $originValue = $propertyValue;
            }
        }
    } catch (NoSuchPropertyException $e) {
        if (null !== $propertyValue = $this->session->get('iform_'.$form->getName())) {
            $originValue = $propertyValue;
            $this->session->remove('iform_'.$form->getName());
        } else {
            $originValue = '';
        }
    } finally {
        //We remove the value from the session, to not overload the memory
        $this->session->remove('iform_'.$form->getName());
    }

    $view->vars['attr'] = array_merge($view->vars['attr'], ['data-orig-value' => $originValue]);
}

private function isParentIForm(FormInterface $form)
{
    if (null === $form->getParent()) {
        return $form->getConfig()->getOption('is_iform');
    }

    return $this->isParentIForm($form->getParent());
}

也许代码示例会对任何人有所帮助! 如果谁有更好的选择,不要犹豫post吧!