【问题标题】:Symfony 3 override default form renderingSymfony 3 覆盖默认表单渲染
【发布时间】:2016-11-18 19:44:03
【问题描述】:

Symfony 关于“自定义表单呈现”的文档似乎含糊不清且具有误导性。我正在尝试在表单上呈现自定义元素,但在尝试这样做时我看到了莫名其妙的行为。

问题:

我希望呈现一个多列选择框。此处准确描述了功能等效项: jQuery Get Selected Option From Dropdown

上下文

  • 模型 - 我有一个用户实体。一个用户可以属于零个、一个或多个组织。
  • 控制器 - 我的控制器进行 API 调用,获取用户和组织(如果有该用户所属的组织)以及用户可以添加的组织列表。
  • 查看 - 我正在呈现一个创建/编辑表单 Twig 模板,其中显示所有用户字段以及用户可能订阅的组织列表。有两个并排的选择框。一个显示用户可以所属的组织,另一个是用户可以添加的可能组织的列表:

<select "user_orgs">
  <option value="1">Organization 1</option>
</select>
<button id="add"> << </button>
<button id="remove"> << </button>
<select "available_orgs">
  <option value="1">Organization 1</option>
  <option value="2">Organization 1</option>
</select>

我正在使用“bootstrap3_form_horizo​​ntal.html.twig 模板进行表单渲染。为了捕获上述功能,我需要提供一种方法来将上述 html 的变体添加到我的表单中。

解决方案 1

向 Twig 扩展添加一个可从表单调用并呈现 html 的方法。

  • (正面)最简单最有效的
  • (否定)不使用树枝。 Duck 类型需要表单值。
  • (否定)不是很干燥。

对于所有解决方案,我都创建了一个 AbstractType 为表单呈现提供控件:

//Predefined form types:
use Symfony\Component\Form\Extension\Core\Type;

class UserType extends AbstractType {

    public function buildForm(FormBuilderInterface $builder, array $options) {
        $builder->add( 'email', Type\EmailType::class, [
                'label' => 'Email / Username',
            ])
            . . .
            ->add('organizations', Type\ChoiceType::class, [
                'label' => 'Organizations',
                'selections' => $options['organizations'],
                'multiple' => true,

            ])
            ->add('submit', Type\SubmitType::class, [
                'attr' => ['class' => 'btn-success btn-outline'],
            ]);
    }
}

user_create.html.twig:

{% form_theme form _self %}
{% block body %}
    {{ form_start(form) }}
    {{ form_errors(form) }}
    {{ form_row(form.email) }}
    {{ multi_select(form.organizations, selected_organizations)|raw }}
    {{ form_end(form) }}
{% endblock %}

“多选”是自定义的 Twig 扩展:

class CustomTwigExtension extends \Twig_Extension {
public function getFunctions() {
        return array(
            new \Twig_SimpleFunction('multi_select', [$this, 'multiSelect']), 
    }

public function multiSelect(\Symfony\Component\Form\FormView $formView, $selections) {
        $formData   = $formView->vars;
        $formLabel  = $formData['label'];
        $selectName = $formData['full_name'];
        $formId     = $formData['id'];
        $optionsId  = $formId . '_options';
        $rhsOptions = "";
        $lhsOptions = "";

        foreach($selections as $key => $value) {
            $lhsOptions .= '<option value="' . $value . ' selected">' . $key . '</option>';
        }
        foreach($formData['choices'] as $option) {
            $rhsOptions .= '<option value="' . $option->value . ' selected">' . $option->label . '</option>';
        }

        $html = <<<HTML
<div class="form-group">
    <label class="col-sm-2 control-label required" for="$formId">$formLabel</label>
    <div class="col-sm-2">
        <select id="$formId" class="form-control" name="$selectName" multiple>
            $lhsOptions
        </select>
    </div>
    <div class="col-sm-1" style="width: 4%;">
        <button class="btn btn-default" id="m-select-to">&laquo;</button>
        <br />&nbsp;<br />
        <button class="btn btn-default" id="m-select-from">&raquo;</button>
    </div>
    <div class="col-sm-2">
        <select id="$optionsId" class="form-control" multiple>
            $rhsOptions
        </select>
    </div>
</div>
HTML;
        return $html;
    }

不是很优雅,但它呈现了想要的结果。 (为了更准确地了解我的最终结果,我包含了 HEREDOC html)。

问题是,“form_start”Twig 方法呈现除了上面的控件之外的组织选择框 - 我已通过在用户的抽象类型。

形式大致类似于: (相关部分用cmets括起来)

<form name="user" method="post" action="/admin/users/save/" class="form-horizontal">
  <!-- THIS IS THE CUSTOM GENERATED CONTROL -->
  <div class="form-group">
    <label class="col-sm-2 control-label required" for="user_organizations">Organizations</label>
    <div class="col-sm-2">
        <select id="user_organizations" class="form-control" name="user[organizations][]" multiple="">
      </select>
    </div>
    <!-- END CUSTOM GENERATED CONTROL -->
    <div class="col-sm-1" style="width: 4%;">
        <button class="btn btn-default" id="m-select-to">«</button>
        <br>&nbsp;<br>
        <button class="btn btn-default" id="m-select-from">»</button>
    </div>
    <div class="col-sm-2">
        <select id="user_organizations_options" class="form-control" multiple="">
            <option value="1 selected">Organization1</option>
            <option value="2 selected">Organization2</option>
        </select>
    </div>
</div>
            <div class="form-group">
              <label class="col-sm-2 control-label required" for="user_email">Email / Username</label>
              <div class="col-sm-10">
                <input id="user_email" name="user[email]" required="required" class="form-control" type="email">
              </div>
              <!-- THIS IS THE SYMFONY GENERATED CONTROL -->
              <div class="form-group">
                <label class="col-sm-2 control-label required" for="user_organizations">Organizations</label>
                <div class="col-sm-10">
                  <select id="user_organizations" name="user[organizations][]" class="form-control" multiple="multiple">
                  <option value="1 selected">Organization1</option>
                  <option value="2 selected">Organization2</option>
                  </select>
                </div>
              </div>
              <!-- END SYMFONY GENERATED CONTROL -->
  </div><input id="user__token" name="user[_token]" value="imatoken" type="hidden"></form>

如您所见,发生了两种不同的事情。一个通用的“开箱即用”选择标签和我自定义呈现的标签。

form_start 方法似乎在遍历 UserType 的子项并呈现除了我指定的控件之外的控件。

解决方案 2

创建一个自定义模板来控制表单的呈现。

  • (肯定)遵循记录在案的最佳实践以实现我的结果
  • (否定)没有完整记录。不起作用。

遵循http://symfony.com/doc/current/form/form_customization.html 中概述并在 SO 帖子中阐明的程序: How to create a custom Symfony2 Twig form template block 也: Custom form field template with twig

我做了以下事情:

创建了一个自定义类型以从自定义模板呈现(来自 Symfony 文档链接):

自定义类型: AppBundle\Form\MultiSelectType:

class MultiSelectType extends AbstractType {

    public function buildView(FormView $view, FormInterface $form, array $options) {
        $view->vars['selections'] = $this->setOptions($options['selections']);
        //The following lines will assign the select box containing
        //already defined organizations. 
        $entity = $view->parent->vars['value'];
        $field = $options['entity_field_name'];
        $method = 'get' . $field;
        $values = call_user_func(array($entity, $method));
        $view->vars['choices'] = $values;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'selections' => array(),
            'custom_label' => 'slapping juice'
        ));
    }

    private function setOptions($options) {
        $ar = [];
        foreach($options as $k => $v) {
            $ar[] = ['value' => $v, 'label' => $k];
        }
        return $ar;
    }
}

我真的不知道在这里指定什么。我要覆盖哪些方法以及如何覆盖? Symfony 文档敦促您查看类似的实例化。 Choices Type 类是800 行长!我需要 800+ 行 PHP 来渲染 20 行 HTML 吗?!

我因此在我的 UserType 中引用它:

class UserType extends AbstractType {

    public function buildForm(FormBuilderInterface $builder, array $options) {
      . . . 
      $builder->add('organizations', MultiSelectType::class, [
                'label' => 'Organizations',
                'selections' => $options['organizations'],
                'entity_field_name' => 'organizations',

            ])
     . . .
     }
}

在我的 Twig Extension 中,我添加了一个引用(很神奇,因为在上面链接的 Symfony 文档中没有提到这一点。)

类 CBMSHelperExtension 扩展 \Twig_Extension {

public function getFunctions() {
    return array(
        new \Twig_SimpleFunction('multiselect_widget', null, [
            'node_class' => 'Symfony\Bridge\Twig\Node\RenderBlockNode',
            'is_safe' => [ 'html' ]])
    );
}

Aaaaand Finaly,我近乎完整的自定义模板,按照说明从现有的树枝模板中复制:

资源/视图/form/fields.html.twig

{% use "form_div_layout.html.twig" %}
{% use  "bootstrap_3_horizontal_layout.html.twig" %}

{% block multi_select_widget %}
    {% spaceless %}
        <div class="form-group">
            {{ form_label(form) }}
            <div class="{{ block('form_group_class') }}">
                <select {{ block('widget_attributes') }} multiple="multiple">
                    {%- set options = selections -%}
                    {%  for choice in options %}
                        <option value="{{ choice.value }}" selected="selected">{{ choice.label }}</option>
                    {% endfor %}
                </select>
            </div>
            <div class="{{ block('form_group_class') }}">
                {% for organization in choices %}
                    <p>{{ organization.name }}</p>
                {% endfor %}
            </div>
        </div>
    {% endspaceless %}
{%  endblock %}

(以上内容已添加到 config.yml)

现在的问题类似于上面的问题。

渲染了一个额外的“组织”标签!!我会省去你的 html 转储,但基本上它的作用是这样的:

|| form tag ||
| form group:           |
| label - email         | input box for email   |
| form group:           |
| label - organizations | form group            |
|                       | label - organizations |
|                       | custom template       |

试图独立呈现组织,尽管我正在尽我所能告诉它不要这样做。

以下是实际应用程序的屏幕截图,展示了这一点:

问题

我如何实现自定义表单渲染,以按照 Symfony 的“最佳实践”从 HTML 渲染自定义表单模板,而不需要混淆,或者创建一个不可扩展的解决方案?

额外问题: 是我自己还是在 Symfony 中开发,比如尝试构建 Rube Goldberg 设备?

【问题讨论】:

标签: php twig symfony


【解决方案1】:

我无法回答渲染额外内容的原因。在基本 Twig 模板 form_div_layout.html.twig 中,没有任何迹象表明正在发生的行为:

{%- block form_start -%}
    {% set method = method|upper %}
    {%- if method in ["GET", "POST"] -%}
        {% set form_method = method %}
    {%- else -%}
        {% set form_method = "POST" %}
    {%- endif -%}
    <form name="{{ name }}" method="{{ form_method|lower }}"{% if action != '' %} action="{{ action }}"{% endif %}{% for attrname, attrvalue in attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}{% if multipart %} enctype="multipart/form-data"{% endif %}>
    {%- if form_method != method -%}
        <input type="hidden" name="_method" value="{{ method }}" />
    {%- endif -%}
{%- endblock form_start -%}

渲染中出现了一些混乱。但是,通过删除此块,只会呈现指定的表单元素(所有 form_rowform_errors 块)。

那么为什么不扩展解决方案 1 并将覆盖添加到 form_start 和 form end 方法?

class MyTwigExtension extends \Twig_Extension {

    public function getFunctions() {
        return array(
            new \Twig_SimpleFunction('custom_form_start', [$this, 'customFormStart']),
            new \Twig_SimpleFunction('custom_form_end', [$this, 'customFormEnd']),
            new \Twig_SimpleFunction('multi_select', [$this, 'multiSelect']),
            new \Twig_SimpleFunction('multi_select_js', [$this, 'multiSelectJS'])
        );
    }
 . . .
public function customFormStart(\Symfony\Component\Form\FormView $formView) {
    $formData = $formView->vars;

    $html = '<form name="' . $formData['name'] . '" method="' . $formData['method'] . '"'
        . ' action="' . $formData['action'] . '" class="form-horizontal">';
    return $html;
}
public function customFormEnd(\Symfony\Component\Form\FormView $formView) {
    $formData = $formView->vars;
    $token = $formView->children['_token']->vars;
    $html = '<input id="' . $token['id'] .'" name="' . $token['full_name']
        . '" value="' . $token['value'] . '" type="hidden">';
    return $html;
}

}

这将生成提交表单所需的其余元素。应该与 Symfony 生成的 应该 没有什么不同。

这可能不会填满所有可扩展性和最佳实践等复选框,但它确实会将数据返回给控制器。

奖励问题:不,开发 Symfony 不像创建 Rube Goldberg 装置。这更像是造一架飞机。 . .当它在空中时。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-01-05
    • 1970-01-01
    • 1970-01-01
    • 2011-11-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多