Search code examples
phpsymfonyformbuildersymfony3

I can't display embedded, nested form in Symfony 3


I have a big issue because I can't find any solutions for my problem on Internet. I work on a Post Entity from which I want to add Images Entities. The idea is when I log in my app, I can write a post and upload one or n images. After that, the user will allow to see the images from his posts for example.

I am at the step one, It seems I can't display my CollectionClass::Class of my ImageType::Class in my PostType.php. I haven't errors, it's simply not displayed in the view. Thanks in advance!

PostType.php

<?php

    namespace UserBundle\Form;

    use Symfony\Component\Form\AbstractType;
    use Symfony\Component\Form\FormBuilderInterface;
    use Symfony\Component\OptionsResolver\OptionsResolver;
    use Symfony\Component\Form\Extension\Core\Type\CollectionType;
    use UserBundle\Entity\Post;


    class PostType extends AbstractType
    {
        /**
         * {@inheritdoc}
         */
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $builder
                ->add('title')
                ->add('content')
                ->add('images', CollectionType::class, [
                    'entry_type' => ImageType::class,
                    'entry_options' => array('label' => false),
                    'allow_add' => true,
                ]);
            ;
        }
        
        /**
         * {@inheritdoc}
         */
        public function configureOptions(OptionsResolver $resolver)
        {
            $resolver->setDefaults(array(
                'data_class' => Post::class,
            ));
        }

        /**
         * {@inheritdoc}
         */
        public function getBlockPrefix()
        {
            return 'userbundle_post';
        }
    }

ImageType.php

<?php

    namespace UserBundle\Form;

    use Symfony\Component\Form\AbstractType;
    use Symfony\Component\Form\FormBuilderInterface;
    use Symfony\Component\OptionsResolver\OptionsResolver;
    use Symfony\Component\Form\Extension\Core\Type\FileType;
    use UserBundle\Entity\Image;

    class ImageType extends AbstractType
    {
        /**
         * {@inheritdoc}
         */
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $builder
                ->add('file', FileType::class, ['required' => false, 'data_class' => null]);
        }
        
        /**
         * {@inheritdoc}
         */
        public function configureOptions(OptionsResolver $resolver)
        {
            $resolver->setDefaults(array(
                'data_class' => Image::class,
            ));
        }

        /**
         * {@inheritdoc}
         */
        public function getBlockPrefix()
        {
            return 'userbundle_image';
        }
    }

Post.php

<?php

    namespace UserBundle\Entity;

    use Doctrine\ORM\Mapping as ORM;
    use Symfony\Component\Validator\Constraints as Assert;
    use Doctrine\Common\Collections\ArrayCollection;

    /**
     * Post
     *
     * @ORM\Table(name="post")
     * @ORM\Entity(repositoryClass="UserBundle\Repository\PostRepository")
     */
    class Post
    {
        /**
         * @var int
         *
         * @ORM\Column(name="id", type="integer")
         * @ORM\Id
         * @ORM\GeneratedValue(strategy="AUTO")
         */
        private $id;

        /**
         * @var string
         *
         * @ORM\Column(name="Title", type="string", length=255)
         */
        private $title;

        /**
         * @var string
         *
         * @ORM\Column(name="Content", type="text")
         */
        private $content;


        /**
         * @ORM\ManyToOne(targetEntity="User", inversedBy="posts")
         * @ORM\JoinColumn(name="user_id", referencedColumnName="id")
        */
        private $user;

        /**
         * @ORM\OneToMany(targetEntity="Image", mappedBy="post", cascade={"persist"})
         */
        private $images;

        /**
         * Get id
         *
         * @return int
         */
        public function getId()
        {
            return $this->id;
        }

        /**
         * Set title
         *
         * @param string $title
         *
         * @return Post
         */
        public function setTitle($title)
        {
            $this->title = $title;

            return $this;
        }

        /**
         * Get title
         *
         * @return string
         */
        public function getTitle()
        {
            return $this->title;
        }

        /**
         * Set content
         *
         * @param string $content
         *
         * @return Post
         */
        public function setContent($content)
        {
            $this->content = $content;

            return $this;
        }

        /**
         * Get content
         *
         * @return string
         */
        public function getContent()
        {
            return $this->content;
        }

        /**
         * Set post
         *
         * @param \
         *
         * @return Post
         */
        public function setUser($user)
        {
            $this->user = $user;

            return $this;
        }

        /**
         * Get post
         *
         * @return \?
         */
        public function getUser()
        {
            return $this->user;
        }
        /**
         * Constructor
         */
        public function __construct()
        {
            $this->images = new \Doctrine\Common\Collections\ArrayCollection();
        }

        /**
         * Add image
         *
         * @param \UserBundle\Entity\Image $image
         *
         * @return Post
         */
        public function addImage(\UserBundle\Entity\Image $image)
        {
            $this->images[] = $image;

            return $this;
        }

        /**
         * Remove image
         *
         * @param \UserBundle\Entity\Image $image
         */
        public function removeImage(\UserBundle\Entity\Image $image)
        {
            $this->images->removeElement($image);
        }

        /**
         * Get images
         *
         * @return \Doctrine\Common\Collections\Collection
         */
        public function getImages()
        {
            return $this->images;
        }
    }

Image.php

<?php
    // src/UserformBundle/Entity/Image

    namespace UserBundle\Entity;

    use Doctrine\ORM\Mapping as ORM;

    /**
     * @ORM\Entity(repositoryClass="UserBundle\Entity\ImageRepository")
     */
    class Image
    {
      /**
       * @ORM\Column(name="id", type="integer")
       * @ORM\Id
       * @ORM\GeneratedValue(strategy="AUTO")
       */
      private $id;

      /**
       * @ORM\Column(name="url", type="string", length=255)
       */
      private $url;

      /**
       * @ORM\Column(name="alt", type="string", length=255)
       */
      private $alt;

      private $file;
      
      public function getFile()
      {
        return $this->file;
      }

      public function setFile(UploadedFile $file = null)
      {
        $this->file = $file;
      }
      

      /**
       * @ORM\ManyToOne(targetEntity="Post", inversedBy="images")
       * @ORM\JoinColumn(name="post_id", referencedColumnName="id")
      */
      private $post;
      

      /**
       * Get id
       *
       * @return integer
       */
      public function getId()
      {
          return $this->id;
      }

      /**
       * Set url
       *
       * @param string $url
       *
       * @return Image
       */
      public function setUrl($url)
      {
          $this->url = $url;

          return $this;
      }

      /**
       * Get url
       *
       * @return string
       */
      public function getUrl()
      {
          return $this->url;
      }

      /**
       * Set alt
       *
       * @param string $alt
       *
       * @return Image
       */
      public function setAlt($alt)
      {
          $this->alt = $alt;

          return $this;
      }

      /**
       * Get alt
       *
       * @return string
       */
      public function getAlt()
      {
          return $this->alt;
      }

      /**
       * Set post
       *
       * @param \UserBundle\Entity\Post $post
       *
       * @return Image
       */
      public function setPost(\UserBundle\Entity\Post $post = null)
      {
          $this->post = $post;

          return $this;
      }

      /**
       * Get post
       *
       * @return \UserBundle\Entity\Post
       */
      public function getPost()
      {
          return $this->post;
      }
    }

EDIT
My Post New action view: new.html.twig

    {% extends 'base.html.twig' %}
        {% block javascripts %}
            <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
            {# Voici le script en question : #}
            <script type="text/javascript">
                $(document).ready(function() {
                    // On récupère la balise <div> en question qui contient l'attribut « data-prototype » qui nous intéresse.
                    var $container = $('div#userbundle_post_images');

                    // On définit un compteur unique pour nommer les champs qu'on va ajouter dynamiquement
                    var index = $container.find(':input').length;

                    // On ajoute un nouveau champ à chaque clic sur le lien d'ajout.
                    $('#add_category').click(function(e) {
                        addImage($container);
                        e.preventDefault(); // évite qu'un # apparaisse dans l'URL
                        return false;
                    });
                    // On ajoute un premier champ automatiquement s'il n'en existe pas déjà un (cas d'une nouvelle annonce par exemple).
                    if (index == 0) {
                        addImage($container);
                    } else {
                        // S'il existe déjà des catégories, on ajoute un lien de suppression pour chacune d'entre elles
                        $container.children('div').each(function() {
                            addDeleteLink($(this));
                        });
                    }

                    // La fonction qui ajoute un formulaire CategoryType
                    function addImage($container) {
                        // Dans le contenu de l'attribut « data-prototype », on remplace :
                        // - le texte "__name__label__" qu'il contient par le label du champ
                        // - le texte "__name__" qu'il contient par le numéro du champ
                        var template = $container.attr('data-prototype')
                            .replace(/__name__label__/g, 'Catégorie n°' + (index+1))
                            .replace(/__name__/g,        index)
                        ;

                        // On crée un objet jquery qui contient ce template
                        var $prototype = $(template);

                        // On ajoute au prototype un lien pour pouvoir supprimer la catégorie
                        addDeleteLink($prototype);

                        // On ajoute le prototype modifié à la fin de la balise <div>
                        $container.append($prototype);

                        // Enfin, on incrémente le compteur pour que le prochain ajout se fasse avec un autre numéro
                        index++;
                    }

                    // La fonction qui ajoute un lien de suppression d'une catégorie
                    function addDeleteLink($prototype) {
                        // Création du lien
                        var $deleteLink = $('<a href="#" class="btn btn-danger">Supprimer l\'image</a>');

                        // Ajout du lien
                        $prototype.append($deleteLink);

                        // Ajout du listener sur le clic du lien pour effectivement supprimer la catégorie
                        $deleteLink.click(function(e) {
                            $prototype.remove();

                            e.preventDefault(); // évite qu'un # apparaisse dans l'URL
                            return false;
                        });
                    }
                });
            </script>
        {% endblock %}

        {% block body %}
            <h1>Post creation</h1>
            {{ form_start(form) }}
                {{ form_row(form.title) }}
                {{ form_row(form.content) }}
                <a href="#" id="add_category" class="btn btn-default">Ajouter une Image</a>
                {{ form_row(form.images) }}
                <br/>
                <input type="submit" value="Create" />
            {{ form_end(form) }}

            <ul>
                <li>
                    <a href="{{ path('post_index') }}">Back to the list</a>
                </li>
            </ul>
        {% endblock %}

EDIT: PostController.php

    /**
     * Creates a new post entity.
     *
     * @Route("/new", name="post_new")
     * @Method({"GET", "POST"})
     */
    public function newAction(Request $request)
    {
        $post = new Post();
        //dump($post);
        
        $user = $this->getUser();
        if (!is_object($user) || !$user instanceof UserInterface) {
            throw new AccessDeniedException('This user does not have access to this section.');
        }

        $form = $this->createForm('UserBundle\Form\PostType', $post);
        dump($form);
        //die();
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            
            // relate this post to the owner user
            $user->addPost($post);
            // relate the owner user to this post
            $post->setUser($user);

            $em = $this->getDoctrine()->getManager();
            $em->persist($post);
            $em->persist($user);
            $em->flush();

            return $this->redirectToRoute('post_show', array('id' => $post->getId()));
        }

        return $this->render('post/new.html.twig', array(
            'post' => $post,
            'form' => $form->createView(),
        ));
    }

Solution

  • It would be useful to see your view but just a reminder: to be able to add new images your form needs some javascript, please look here: https://symfony.com/doc/current/form/form_collections.html#allowing-new-tags-with-the-prototype

    Without javascript, by default it would only render already added collection objects. If you have none in your entity, it will display nothing by default.