.. index:: single: Formulário; Embutir uma coleção de formulários Como embutir uma Coleção de Formulários ======================================= Neste artigo, você vai aprender como criar um formulário que incorpora uma coleção de muitos outros formulários. Isto pode ser útil, por exemplo, se você tem uma classe ``Task`` onde você deseja editar/criar/remover muitos objetos ``Tag`` relacionados a Task, dentro do mesmo formulário. .. note:: Neste artigo, é livremente assumido que você está usando o Doctrine para armazenar em seu banco de dados. Mas se você não está usando o Doctrine (por exemplo, Propel ou apenas uma conexão de banco de dados), tudo é muito semelhante. Há apenas algumas partes deste tutorial que realmente se preocupam com "persistência". Se você *está* usando o Doctrine, você vai precisar adicionar os metadados do Doctrine, incluindo a definição de mapeamento da associação ``ManyToMany`` na propriedade ``tags`` da Task. Vamos começar: suponha que cada ``Task`` pertence a vários objetos ``Tags``. Comece criando uma classe simples ``Task``:: // src/Acme/TaskBundle/Entity/Task.php namespace Acme\TaskBundle\Entity; use Doctrine\Common\Collections\ArrayCollection; class Task { protected $description; protected $tags; public function __construct() { $this->tags = new ArrayCollection(); } public function getDescription() { return $this->description; } public function setDescription($description) { $this->description = $description; } public function getTags() { return $this->tags; } public function setTags(ArrayCollection $tags) { $this->tags = $tags; } } .. note:: O ``ArrayCollection`` é específico do Doctrine e é basicamente o mesmo que usar um ``array`` (mas deve ser um ``ArrayCollection`` se você está usando o Doctrine). Agora, crie uma classe ``Tag``. Como você viu acima, uma ``Task`` pode ter muitos objetos ``Tag``:: // src/Acme/TaskBundle/Entity/Tag.php namespace Acme\TaskBundle\Entity; class Tag { public $name; } .. tip:: A propriedade ``name`` é pública aqui, mas ela pode facilmente ser protegida ou privada (então seriam necessários os métodos ``getName`` e ``setName``). Agora, vamos para os formulários. Crie uma classe de formulário para que um objeto ``Tag`` possa ser modificado pelo usuário:: // src/Acme/TaskBundle/Form/Type/TagType.php namespace Acme\TaskBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class TagType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('name'); } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => 'Acme\TaskBundle\Entity\Tag', )); } public function getName() { return 'tag'; } } Com isso, você tem o suficiente para renderizar um formulário tag. Mas, uma vez que o objetivo final é permitir que as tags de uma ``Task`` sejam modificadas dentro do próprio formulário da task, crie um formulário para a classe ``Task``. Observe que você embutiu uma coleção de formulários ``TagType`` usando o tipo de campo :doc:`collection`:: // src/Acme/TaskBundle/Form/Type/TaskType.php namespace Acme\TaskBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class TaskType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('description'); $builder->add('tags', 'collection', array('type' => new TagType())); } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => 'Acme\TaskBundle\Entity\Task', )); } public function getName() { return 'task'; } } Em seu controlador, você irá agora inicializar uma nova instância do ``TaskType``:: // src/Acme/TaskBundle/Controller/TaskController.php namespace Acme\TaskBundle\Controller; use Acme\TaskBundle\Entity\Task; use Acme\TaskBundle\Entity\Tag; use Acme\TaskBundle\Form\Type\TaskType; use Symfony\Component\HttpFoundation\Request; use Symfony\Bundle\FrameworkBundle\Controller\Controller; class TaskController extends Controller { public function newAction(Request $request) { $task = new Task(); // dummy code - this is here just so that the Task has some tags // otherwise, this isn't an interesting example $tag1 = new Tag(); $tag1->name = 'tag1'; $task->getTags()->add($tag1); $tag2 = new Tag(); $tag2->name = 'tag2'; $task->getTags()->add($tag2); // end dummy code $form = $this->createForm(new TaskType(), $task); // process the form on POST if ($request->isMethod('POST')) { $form->bind($request); if ($form->isValid()) { // ... maybe do some form processing, like saving the Task and Tag objects } } return $this->render('AcmeTaskBundle:Task:new.html.twig', array( 'form' => $form->createView(), )); } } O template correspondente agora é capaz de renderizar tanto o campo ``description`` para o formulário da task, quanto todos os formulários ``TagType`` para quaisquer tags que já estão relacionadas com esta ``Task``. No controlador acima, foi adicionado algum código fictício para que você possa ver isso em ação (uma vez que uma ``Task`` não tem nenhuma tag quando ela é criada pela primeira vez). .. configuration-block:: .. code-block:: html+jinja {# src/Acme/TaskBundle/Resources/views/Task/new.html.twig #} {# ... #}
{# render the task's only field: description #} {{ form_row(form.description) }}

Tags

{{ form_rest(form) }} {# ... #}
.. code-block:: html+php

Tags

rest($form) ?>
Quando o usuário submeter o formulário, os dados submetidos para os campos ``Tags`` são usados ​​para construir um ArrayCollection de objetos ``Tag``, o qual é então definido no campo ``tag`` da instância ``Task``. A coleção ``Tags`` é acessível naturalmente via ``$task->getTags()`` e pode ser persistida no banco de dados ou utilizada da forma que você precisar. Até agora, isso funciona muito bem, mas não permite que você adicione dinamicamente novas tags ou exclua as tags existentes. Então, enquanto a edição de tags existentes irá funcionar perfeitamente, o usuário não pode, ainda, adicionar quaisquer tags novas. .. caution:: Neste artigo, você embutiu apenas uma coleção, mas você não está limitado a apenas isto. Você também pode incorporar coleção aninhada com a quantidade de níveis abaixo que desejar. Mas, se você usar o Xdebug em sua configuração de desenvolvimento, você pode receber erro ``Maximum function nesting level of '100' reached, aborting!``. Isto ocorre devido a configuração do PHP ``xdebug.max_nesting_level``, que tem como padrão ``100``. Esta diretiva limita recursão para 100 chamadas, o que pode não ser o suficiente para renderizar o formulário no template se você renderizar todo o formulário de uma vez (por exemplo, usando ``form_widget(form)``). Para corrigir isso, você pode definir esta diretiva para um valor maior (através do arquivo ini do PHP ou via :phpfunction:`ini_set`, por exemplo em ``app/autoload.php``) ou renderizar cada campo do formulário manualmente usando ``form_row``. .. _cookbook-form-collections-new-prototype: Permitindo "novas" tags com o "prototype" ----------------------------------------- Permitir ao usuário adicionar dinamicamente novas tags significa que você vai precisar usar algum JavaScript. Anteriormente, você adicionou duas tags ao seu formulário no controlador. Agora, para permitir ao usuário adicionar a quantidade de formulários tag que precisar diretamente no navegador, vamos utilizar um pouco de JavaScript. A primeira coisa que você precisa fazer é tornar a coleção de formulário ciente de que ela vai receber um número desconhecido de tags. Até agora, você adicionou duas tags e o tipo formulário espera receber exatamente duas, caso contrário, um erro será lançado: ``Este formulário não deve conter campos extras``. Para tornar isto flexível, adicione a opção ``allow_add`` no seu campo de coleção:: // src/Acme/TaskBundle/Form/Type/TaskType.php // ... use Symfony\Component\Form\FormBuilderInterface; public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('description'); $builder->add('tags', 'collection', array( 'type' => new TagType(), 'allow_add' => true, 'by_reference' => false, )); } Note que ``'by_reference' => false`` também foi adicionado. Normalmente, o framework de formulário irá modificar as tags em um objeto `Task` *sem* realmente nunca chamar `setTags`. Definindo :ref:`by_reference` para `false`, o `setTags` será chamado. Você verá que isto será importante mais tarde. Além de dizer ao campo para aceitar qualquer número de objetos submetidos, o ``allow_add`` também disponibiliza para você uma variável "prototype". Este "prototype" é um "template" que contém todo o HTML para poder renderizar quaisquer formulários "tag" novos. Para renderizá-lo, faça a seguinte alteração no seu template: .. configuration-block:: .. code-block:: html+jinja .. code-block:: html+php .. note:: Se você renderizar todo o seu sub-formulário "tags" de uma vez (por exemplo ``form_row(form.tags)``), então o prototype está automaticamente disponível na ``div`` externa, no atributo ``data-prototype``, semelhante ao que você vê acima. .. tip:: O ``form.tags.vars.prototype`` é um elemento de formulário com o aspecto semelhante aos elementos individuais ``form_widget(tag)`` dentro do seu laço ``for``. Isso significa que você pode chamar ``form_widget``, ``form_row`` ou ``form_label`` nele. Você pode até mesmo optar por renderizar apenas um de seus campos (por exemplo, o campo ``name``): .. code-block:: html+jinja {{ form_widget(form.tags.vars.prototype.name)|e }} Na página renderizada, o resultado será algo parecido com o seguinte: .. code-block:: html