4

My model contains two related classes - RealEstate and Image, and for one instance of RealEstate can be a lot of Image's instances. Since the Image class can also be used in association with other classes, I chose relationship 'One-To-Many, Unidirectional with Join Table'. This ensures that any image does not need to know where it is used. In turn, RealProperty class is supplied with the $images property, the getImages(), addImage(Image $image) and removeImage(Image $image) methods, and the $images in the constructor is defined by an empty ArrayCollection. So, I have the following model classes.

1) App\Entity\RealProperty\RealProperty

namespace App\Entity\RealProperty;

use App\Entity\Platform\Image;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\RealProperty\RealPropertyRepository")
 * @ORM\Table(name="real_property")
 */
class RealProperty
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * Many real properties have many images
     * @ORM\ManyToMany(targetEntity="App\Entity\Platform\Image", cascade={"all"})
     * @ORM\JoinTable(name="real_property_images",
     *      joinColumns={@ORM\JoinColumn(name="real_property_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="image_id", referencedColumnName="id", unique=true)}
     *      )
     */
    private $images;

    /**
     * RealProperty constructor
     */
    public function __construct()
    {
        $this->images = new ArrayCollection();
    }

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

    /**
     * @return mixed
     */
    public function getImages()
    {
        return $this->images;
    }

    /**
     * @param Image $image
     */
    public function addImage(Image $image)
    {
        if (!$this->images->contains($image)) {
            $this->images->add($image);
        }
    }

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

2) App\Entity\Platform\Image

namespace App\Entity\Platform;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;

/**
 * @ORM\Entity(repositoryClass="App\Repository\Platform\ImageRepository")
 * @Vich\Uploadable
 */
class Image
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * NOTE: This is not a mapped field of entity metadata, just a simple property.
     *
     * @Vich\UploadableField(mapping="image", fileNameProperty="imageName", size="imageSize")
     *
     * @var File
     */
    private $imageFile;

    /**
     * @ORM\Column(type="string", length=255, nullable=false)
     *
     * @var string
     */
    private $imageName;

    /**
     * @ORM\Column(type="integer")
     *
     * @var integer
     */
    private $imageSize;

    /**
     * @ORM\Column(type="datetime", nullable=false)
     * @var \DateTime
     */
    private $dateOfCreation;

    /**
     * @ORM\Column(type="datetime", nullable=false)
     * @var \DateTime
     */
    private $dateOfChange;

    /**
     * Image constructor
     */
    public function __construct()
    {
        $currentDate = new \DateTime('NOW');
        $this->dateOfCreation = $currentDate;
        $this->dateOfChange = $currentDate;
    }

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

    /**
     * @param mixed $id
     */
    public function setId($id)
    {
        $this->id = $id;
    }

    /**
     * @return File
     */
    public function getImageFile(): ?File
    {
        return $this->imageFile;
    }

    /**
     * If manually uploading a file (i.e. not using Symfony Form) ensure an instance
     * of 'UploadedFile' is injected into this setter to trigger the  update. If this
     * bundle's configuration parameter 'inject_on_load' is set to 'true' this setter
     * must be able to accept an instance of 'File' as the bundle will inject one here
     * during Doctrine hydration.
     *
     * @param File|\Symfony\Component\HttpFoundation\File\UploadedFile $image
     */
    public function setImageFile(?File $image = null): void
    {
        $this->imageFile = $image;

        if (null !== $image) {
            // It is required that at least one field changes if you are using doctrine
            // otherwise the event listeners won't be called and the file is lost
            $this->dateOfChange = new \DateTimeImmutable();
        }
    }

    /**
     * @return string
     */
    public function getImageName(): ?string
    {
        return $this->imageName;
    }

    /**
     * @param string $imageName
     */
    public function setImageName(?string $imageName)
    {
        $this->imageName = $imageName;
    }

    /**
     * @return int
     */
    public function getImageSize(): ?int
    {
        return $this->imageSize;
    }

    /**
     * @param int $imageSize
     */
    public function setImageSize(?int $imageSize)
    {
        $this->imageSize = $imageSize;
    }

    /**
     * @return \DateTime
     */
    public function getDateOfCreation(): ?\DateTime
    {
        return $this->dateOfCreation;
    }

    /**
     * @param \DateTime $dateOfCreation
     */
    public function setDateOfCreation(?\DateTime $dateOfCreation)
    {
        $this->dateOfCreation = $dateOfCreation;
    }

    /**
     * @return \DateTime
     */
    public function getDateOfChange(): ?\DateTime
    {
        return $this->dateOfChange;
    }

    /**
     * @param \DateTime $dateOfChange
     */
    public function setDateOfChange(?\DateTime $dateOfChange)
    {
        $this->dateOfChange = $dateOfChange;
    }
}

For each class, I created the appropriate form types.

1) App\Form\RealProperty\RealPropertyType

namespace App\Form\RealProperty;

use App\Entity\RealProperty\RealProperty;
use App\Form\Platform\ImageType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class RealPropertyType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('images', CollectionType::class, array(
                'entry_type' => ImageType::class,
                'label' => false,
                'allow_add'  => true,
                'allow_delete' => true,
                'prototype' => true,
                'by_reference' => false
            ))
            ->add('submit', SubmitType::class, [
                'label' => 'Сохранить',
                'attr' => [
                    'class' => 'btn btn-sm btn-primary col-6 mx-auto',
                    'style' => 'display: block;'
                ]
            ])
        ;
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'App\Entity\RealProperty\RealProperty'
        ));
    }

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

2) App\Form\Platform\ImageType

<?php

namespace App\Form\Platform;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Vich\UploaderBundle\Form\Type\VichImageType;

class ImageType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('imageFile', VichImageType::class, array(
                'label' => false,
                'required' => true
            ))
        ;
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'App\Entity\Platform\Image'
        ));
    }

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

Here is the code in my controller where the form containing CollectionType is created.

<?php

namespace App\Controller\RealProperty;

use App\Entity\RealProperty\RealProperty;
use App\Form\RealProperty\RealPropertyType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * Class RealPropertyController
 *
 * @Route("real_property")
 * @package App\Controller\RealProperty
 */
class RealPropertyController extends Controller
{
    /**
     * Creates a new real property entity
     *
     * @Route("/new", name="real_property_new")
     * @Method({"GET", "POST"})
     * @param Request $request
     * @return \Symfony\Component\HttpFoundation\RedirectResponse|Response
     */
    public function newAction(Request $request) {
        $realProperty = new RealProperty();
        $form = $this->createForm(RealPropertyType::class, $realProperty);
        $form->handleRequest($request);

//        dump($form->getData());
//        dump($realProperty);
//        dump($realProperty->getImages());
//        dump($request->get('images'));

        if ($form->isSubmitted() && $form->isValid()) {
            $em = $this->getDoctrine()->getManager();
            $em->persist($realProperty);
            $em->flush();

            return $this->redirectToRoute('real_property_index');
        }

        return $this->render('RealProperty/RealProperty/new.html.twig', [
            'realProperty'  => $realProperty,
            'form'          => $form->createView(),
        ]);
    }
}

However, the ArrayCollection, which must contain Image instances, is always empty, but on the client side all child fields of the CollectionType contain their images.

We can assume that I have incorrectly configured Vich/UploaderBundle, there are no permissions to save images in the server directory, the database schema is incorrectly described ... But - no! All is correct. Especially for this, I created a separate ImageController, which in newAction() creates the ImageType form, and all images are safely stored in the database. So the problem lies somewhere at the ArrayCollection level or at the level of the 'One-To-Many, Unidirectional with Join Table' relationship. I think so.

Help, please, to find this pitfall. I would be very grateful. If necessary - I can share the project through git.

Vitaly Vesyolko
  • 558
  • 5
  • 22

2 Answers2

1

Can you open the Symfony Profiler when submitting your form ?

The "Forms" tab will provide you a per-field detail of how the data is handled, with Model, Normalized and View Format.

This might help you check the type of your field before and once handled by the form component.

Mwa
  • 11
  • 1
  • 2
0

I Think you should use VichImageType Field. In this Link you can see the right way. Why you are setting Id in App\Entity\Platform\Image manually? this is VichImageType code:

use Vich\UploaderBundle\Form\Type\VichImageType;

class Form extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        // ...

        $builder->add('imageFile', VichImageType::class, [
            'required' => false,
            'allow_delete' => true,
            'download_label' => '...',
            'download_uri' => true,
            'image_uri' => true,
            'imagine_pattern' => '...',
        ]);
    }
}