infinite-networks / InfiniteFormBundle

A collection of useful form types and extensions for Symfony.
169 stars 40 forks source link

Could not load type ... #48

Closed 3kynox closed 8 years ago

3kynox commented 8 years ago

Hello,

I got some problems making polycollection to work. InfiniteFormBundle is correctly installed.

I got a base contact entity (named bruno) whith inside a 'moyensComm' attribute set as ArrayCollection

// brunoBundle\Entity\Bruno.php

namespace brunoBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
/**
 * Bruno
 */
class Bruno
{
    /**
     * @var integer
     */
    private $id;
    // ... other attributes

    /**
     * @var \Doctrine\Common\Collections\Collection
     */
    private $moyensComm;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->moyensComm = new ArrayCollection();
    }

    // getters & setters ...
}

Mapping for that class include a oneToMany relation to MoyenComm abstract entity described as follow

# brunoBundle\Resources\config\doctrine\Bruno.orm.yml
...
oneToMany:
        moyensComm:
            targetEntity: MoyenComm
            mappedBy: bruno
            cascade: ['persist', 'remove']

And here is the MoyenComm class

// brunoBundle\Entity\MoyenComm.php

namespace brunoBundle\Entity;

/**
 * MoyenComm
 */
abstract class MoyenComm
{
    /**
     * @var integer
     */
    protected $id;

    /**
     * @var \brunoBundle\Entity\Bruno
     */
    protected $bruno;

    // getters & setters ...
}

Which have inheritance ORM set to three subclasses (Telephone, Email & Adresse) and inversed map

# brunoBundle\Resources\config\doctrine\MoyenComm.orm.yml

brunoBundle\Entity\MoyenComm:
    type: entity
    table: null
    repositoryClass: brunoBundle\Entity\MoyenCommRepository
    inheritanceType: JOINED
    discriminatorColumn:
        name: type
        type: integer
    discriminatorMap:
        1: Telephone
        2: Email
        3: Adresse
    id:
        id:
            type: integer
            id: true
            generator:
                strategy: AUTO
    manyToOne:
        bruno:
            targetEntity: Bruno
            inversedBy: moyenComm
    fields:
        # fields here

    lifecycleCallbacks: {  }

Now I build my base form

// brunoBundle\Form\BrunoType.php

namespace brunoBundle\Form;

use brunoBundle\Form\Type\GenderType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class BrunoType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('text')
            ->add('textarea')
            ->add('email', 'email')
            ->add('entier')
            ->add('money', 'money')
            ->add('date')
            ->add('choice', new GenderType(), array(
                'expanded' => true,
                'multiple' => false
            ))
            ->add('moyensComm', 'infinite_form_polycollection',  array(
                'types' => array(
                    'brunobundle_moyencomm',
                    'brunobundle_telephone',
                    'brunobundle_email',
                    'brunobundle_adresse'
                ),
                'allow_add'    => true,
                'allow_delete' => true
            ))
        ;
    }

    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'brunoBundle\Entity\Bruno'
        ));
    }

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

And here is the MoyenCommType

// brunoBundle\Form\MoyenCommType.php

namespace brunoBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class MoyenCommType extends AbstractType
{
    protected $dataClass = 'brunoBundle\Entity\MoyenComm';

    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('_type', 'hidden', array(
            'data'   => $this->getName(),
            'mapped' => false
        ));
    }

    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class'  => $this->dataClass,
            'model_class' => $this->dataClass,
        ));
    }

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

... and subclasses TelephoneType, EmailType, etc... that extends MoyenCommType.

The error I get is

Could not load type "brunobundle_moyencomm" 500 Internal Server Error - InvalidArgumentException

I checked the name exactly and all seem to be correct according to InfiniteFormBundle documentation.

Some help possible please ?

3kynox commented 8 years ago

I get out of this error by settings my forms as service

  brunobundle_moyencomm:
    class: brunoBundle\Form\MoyenCommType
    tags:
      - { name: form.type }

  brunobundle_telephone:
      class: brunoBundle\Form\TelephoneType
      tags:
        - { name: form.type }

  brunobundle_email:
      class: brunoBundle\Form\EmailType
      tags:
        - { name: form.type }

  brunobundle_adresse:
      class: brunoBundle\Form\AdresseType
      tags:
        - { name: form.type }

But now it returns nothing

Any idea ?

jmclean commented 8 years ago

Currently we don't provide a default template for polycollection. For now, add something like this to the twig template that contains your form:

{% form_theme form.moyensComm _self %}

{% block _brunobundle_bruno_moyensComm_widget %}
    {# Example polycollection template #}
    <div class="collection">
        <a class="btn btn-default add_item" href="#" data-prototype="{{ form_row(form.vars.prototypes.brunobundle_email) | escape }}">Add email</a>
        <a class="btn btn-default add_item" href="#" data-prototype="{{ form_row(form.vars.prototypes.brunobundle_telephone) | escape }}">Add telephone</a>
        <a class="btn btn-default add_item" href="#" data-prototype="{{ form_row(form.vars.prototypes.brunobundle_adresse) | escape }}">Add adresse</a>
        <hr />
        <div class="items">
            {% for subForm in form %}
                {{ form_row(subForm) }}
            {% endfor %}
        </div>
    </div>
{% endblock %}

{% block _brunobundle_bruno_moyensComm_entry_row %}
    {# Example polycollection row template #}
    <div class="item">
        <a class="btn btn-danger remove_item pull-right" href="#">Remove</a>
        {{ form_widget(form._type) }}
        {{ form_widget(form) }} {# Can be overridden - see below #}
        {{ form_errors(form) }}
        <hr />
    </div>
{% endblock %}

{% block brunobundle_adresse_widget %}
    {# Example content for row template #}
    {{ form_row(form.line1, {label: 'Line 1'}) }}
    {{ form_row(form.line2, {label: 'Line 2'}) }}
    {{ form_row(form.line3, {label: 'Line 3'}) }}
{% endblock %}

And the appropriate JavaScript to initialise the collection:

$(function() {
    $('.collection').each(function() {
        var coll = $(this);

        new infinite.Collection(
            coll.find('.items'),
            coll.find('.add_item'),
            {
                itemSelector: '.item',
                removeSelector: '.remove_item'
            }
        );
    });
});
3kynox commented 8 years ago

Hello again and thanks for answer.

I almost get it to work, data is just not displayed

If I make a dump of "subForm" inside {% for subForm in form %} here is the results

and if I make a dump of "form._type" inside the entry_row template, no moyensComm data

Thanks for help

jmclean commented 8 years ago

Ah! Back in BrunoType.php, change the types array to say:

                'types' => array(
                    'brunobundle_telephone',
                    'brunobundle_email',
                    'brunobundle_adresse'
                ),

It gets confused if the base form is in there too.

3kynox commented 8 years ago

Thanks for answer, BrunoType seems OK

// brunoBundle/Form/BrunoType.php

namespace brunoBundle\Form;

use brunoBundle\Form\Type\GenderType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class BrunoType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('text')
            ->add('textarea')
            ->add('email', 'email')
            ->add('entier')
            ->add('money', 'money')
            ->add('date')
            ->add('choice', new GenderType(), array(
                'expanded' => true,
                'multiple' => false
            ))
            ->add('moyensComm', 'infinite_form_polycollection', array(
                'types' => array(
                    'brunobundle_moyencomm',
                    'brunobundle_telephone',
                    'brunobundle_email',
                    'brunobundle_adresse'
                ),
                'allow_add'    => true,
                'allow_delete' => true,
                'by_reference' => false,
                'label' => 'Moyens de Communication'
            ));
    }

    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'brunoBundle\Entity\Bruno'
        ));
    }

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

Here is MoyenCommType

// brunoBundle/Form/MoyenCommType.php

namespace brunoBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormInterface;
use brunoBundle\Entity\Telephone;

class MoyenCommType extends AbstractType
{
    protected $dataClass = 'brunoBundle\Entity\MoyenComm';

    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('_type', 'hidden', array(
            'data'   => $this->getName(),
            'mapped' => false
        ));
    }

    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class'  => $this->dataClass,
            'model_class' => $this->dataClass,
        ));
    }

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

And one of the childrens

// brunoBundle/Form/TelephoneType.php

namespace brunoBundle\Form;

use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class TelephoneType extends MoyenCommType
{
    protected $dataClass = 'brunoBundle\Entity\TelephoneType';

    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        parent::buildForm($builder, $options);

        $builder
            ->add('numero')
        ;
    }

    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'brunoBundle\Entity\Telephone'
        ));
    }

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

The theme looks like this

// brunoBundle/Resources/views/Form/polycollection.html.twig

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

{% block _brunobundle_bruno_moyensComm_widget %}
    <div class="collection">
        <div class="items">
            {% for subForm in form %}
                {#{{ dump(subForm) }}#}
                {{ form_row(subForm) }}
            {% endfor %}
        </div>
        <hr />
        <a class="btn btn-default add_item" href="#" data-prototype="{{ form_row(form.vars.prototypes.brunobundle_telephone) | escape }}">Ajouter Téléphone</a>&nbsp;
        <a class="btn btn-default add_item" href="#" data-prototype="{{ form_row(form.vars.prototypes.brunobundle_email) | escape }}">Ajouter Email</a>&nbsp;
        <a class="btn btn-default add_item" href="#" data-prototype="{{ form_row(form.vars.prototypes.brunobundle_adresse) | escape }}">Ajouter Adresse</a>
    </div>
{% endblock %}

{% block _brunobundle_bruno_moyensComm_entry_row %}
    <div class="item">
        <a class="remove_item pull-right" href="#"><span class="glyphicon glyphicon-remove-circle"></span></a>
        {#{{ dump(form._type) }}#}
        {{ form_widget(form._type) }}
        {{ form_widget(form) }}
        {{ form_errors(form) }}
        <hr />
    </div>
{% endblock %}

{% block brunobundle_telephone_widget %}
    <br />
    {{ form_row(form.numero, {label: 'Numéro'}) }}
{% endblock %}

{% block brunobundle_email_widget %}
    <br />
    {{ form_row(form.email, {label: 'Email'}) }}
{% endblock %}

{% block brunobundle_adresse_widget %}
    <br />
    {{ form_row(form.adresse, {label: 'Adresse'}) }}
    {{ form_row(form.codePostal, {label: 'Code Postal'}) }}
    {{ form_row(form.ville, {label: 'Ville'}) }}
{% endblock %}

and is called in form edit page

// brunoBundle/Resources/views/Bruno/edit.html.twig

{% extends '::base.html.twig' %}

{% block pageTitle %}formType : Édition{% endblock %}

{% block body -%}
    {% form_theme form 'brunoBundle:Form:polycollection.html.twig' %}

    {{ form(form) }}

    <ul class="record_actions">
        <li>
            <a href="{{ path('bruno') }}">
                Back to the list
            </a>
        </li>
        <li>{{ form(delete_form) }}</li>
    </ul>
{% endblock %}

JS code resides on base layout

// app/Resources/views/base.html.twig

// ...

{% block javascripts %}
    {% javascripts '@jquery' '@bootstrap_js' %}
        <script type="text/javascript" src="{{ asset_url }}"></script>
    {% endjavascripts %}
    {% javascripts '@infinite_js' %}
        <script type="text/javascript" src="{{ asset_url }}"></script>
    {% endjavascripts %}
    <script>
        jQuery(document).ready(function() {
            $('.collection').each(function() {
                var coll = $(this);

                new infinite.Collection(
                        coll.find('.items'),
                        coll.find('.add_item'),
                        {
                            itemSelector: '.item',
                            removeSelector: '.remove_item'
                        }
                );
            });
       });
    </script>
{% endblock %}

// ...

Sorry for all this massive paste and thanks again for lights :)

3kynox commented 8 years ago

According to your example, if I remove 'brunobundle_moyencomm', from array, I get error :

The form's view data is expected to be an instance of class brunoBundle\Entity\Telephone, but is an instance of class brunoBundle\Entity\Email. You can avoid this error by setting the "data_class" option to null or by adding a view transformer that transforms an instance of class brunoBundle\Entity\Email to an instance of brunoBundle\Entity\Telephone.

I hope that helps.

jmclean commented 8 years ago

What does EmailType look like?

jmclean commented 8 years ago

Oh, just noticed a problem in TelephoneType. Set the $dataClass to 'brunoBundle\Entity\Telephone' and amend setDefaultOptions() to call parent::setDefaultOptions(). Better check the other child forms too.

3kynox commented 8 years ago

Works !

EmailType and AdresseType was not having protected $dataClass, only TelephoneType was having, then I corrected and pointed it out to entity instead of formType and finally I removed setDefaultOption method from childs.

class EmailType extends MoyenCommType
{
    protected $dataClass = 'brunoBundle\Entity\Email';

    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        parent::buildForm($builder, $options);

        $builder
            ->add('email')
        ;
    }

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

Thanks for great support, waiting now for future great updates :P

Now managing updateAction from controller.

EDIT:

Controller and DB update works too, here is my editAction code :

public function updateAction(Request $request, $id)
{
    $em = $this->getDoctrine()->getManager();

    $entity = $em->getRepository('brunoBundle:Bruno')->find($id);

    if (!$entity) {
        throw $this->createNotFoundException('Unable to find Bruno entity.');
    }

    $moyensComm = new ArrayCollection();

    foreach ($entity->getMoyensComm() as $moyenComm) {
        $moyensComm->add($moyenComm);
    }

    $deleteForm = $this->createDeleteForm($id);
    $editForm = $this->createEditForm($entity);
    $editForm->handleRequest($request);

    if ($editForm->isValid()) {
        foreach($editForm->getViewData()->getMoyensComm() as $data) {
            if($data->getBruno() == null){
                $data->setBruno($entity);
            }
       }
        foreach ($moyensComm as $moyenComm) {
            if (false === $entity->getMoyensComm()->contains($moyenComm)) {
                $em->remove($moyenComm);
            }
        }

        $em->flush();

        return $this->redirect($this->generateUrl('bruno_edit', array('id' => $id)));
    }

    return $this->render('brunoBundle:Bruno:edit.html.twig', array(
        'entity'      => $entity,
        'edit_form'   => $editForm->createView(),
        'delete_form' => $deleteForm->createView(),
    ));
}
jmclean commented 8 years ago

\o/