ritch
ritch

Reputation: 1808

Symfony2 - Embeding a File Form

I have two Entities

 - Kitchen
 - KitchenSubImage

Each kitchen has a main image but also has many sub images (KitchenSubImage).

I have implemented both the entities and their form types. At this moment I have the form displayed and have implemented everything from How to Handle File Uploads with Symfony2 to handle the file upload.

The issue I have is that I have no idea how to handle both file uploads at once. It's made more complicated by the fact that the kitchen can have many sub images.

I also have the following error at the top of the form when I submit the form:

This value should be of type PWD\WebsiteBundle\Entity\KitchenSubImage.

Controller

<?php

namespace PWD\AdminBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

use PWD\WebsiteBundle\Entity\Kitchen;
use PWD\AdminBundle\Form\Type\KitchenType;

use PWD\WebsiteBundle\Entity\KitchenSubImage;
use PWD\AdminBundle\Form\Type\KitchenSubImageType;

class KitchenController extends Controller
{
    public function indexAction()
    {
        return 'index';
    }

    public function addAction(Request $request)
    {
        $kitchen = new Kitchen();
        $image = new KitchenSubImage();
        $kitchen->addSubImage($image);
        $form = $this->createForm(new KitchenType(), $kitchen);

        $form->handleRequest($request);

        if ($form->isValid()) {
            $kitchen->upload();
            return $this->render('PWDWebsiteBundle:Pages:home.html.twig');
        }

        return $this->render('PWDAdminBundle:Pages:form-test.html.twig', array(
            'form' => $form->createView(),
        ));
    }
}

Kitchen Entity

<?php 

namespace PWD\WebsiteBundle\Entity;

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

/**
 * @ORM\Entity
 * @ORM\Table(name="kitchen")
 */
class Kitchen
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(type="string", length=100)
     * @Assert\NotBlank()
     */
    protected $name;

    /**
     * @ORM\Column(type="text")
     * @Assert\NotBlank()
     */
    protected $description;

    /**
     * @Assert\File(maxSize="6000000")
     * @Assert\Image(
     *     minWidth = 800,
     *     maxWidth = 800,
     *     minHeight = 467,
     *     maxHeight = 467
     * )
     */
    protected $mainImage;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    protected $mainImagePath;

    /**
     * @Assert\Type(type="PWD\WebsiteBundle\Entity\KitchenSubImage")
     * @ORM\OneToMany(targetEntity="KitchenSubImage", mappedBy="kitchen")
     */
    protected $subImage;

    public function __construct()
    {
        $this->subImage = new ArrayCollection();
    }

    public function getName()
    {
        return $this->name;
    }

    public function setName($name)
    {
        $this->name = $name;
    }

    public function getDescription()
    {
        return $this->description;
    }

    public function setDescription($description)
    {
        $this->description = $description;
    }

    public function getMainImage()
    {
        return $this->mainImage;
    }

    public function setMainImage(UploadedFile $mainImage = null)
    {
        $this->mainImage = $mainImage;
    }

    public function getSubImage()
    {
        return $this->subImage;
    }

    public function setSubImage(KitchenSubImage $subImage = null)
    {
        $this->subImage = $subImage;
    }

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

    /**
     * Set mainImagePath
     *
     * @param string $mainImagePath
     * @return Kitchen
     */
    public function setMainImagePath($mainImagePath)
    {
        $this->mainImagePath = $mainImagePath;

        return $this;
    }

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

    public function getAbsolutePath()
    {
        return null === $this->mainImagePath
            ? null
            : $this->getUploadRootDir().'/'.$this->mainImagePath;
    }

    public function getWebPath()
    {
        return null === $this->mainImagePath
            ? null
            : $this->getUploadDir().'/'.$this->mainImagePath;
    }

    public function getUploadRootDir()
    {
        return __DIR__.'/../../../../web/'.$this->getUploadDir();
    }

    public function getUploadDir()
    {
        return 'uploads/our-work';
    }

    public function upload()
    {
        if (null === $this->getMainImage()) {
            return;
        }

        $this->getMainImage()->move(
            $this->getUploadRootDir(),
            $this->getMainImage()->getClientOriginalName()
        );

        $this->mainImagePath = $this->getMainImage()->getClientOriginalName();

        $this->mainImage = null;
    }

    /**
     * Add subImage
     *
     * @param \PWD\WebsiteBundle\Entity\KitchenSubImage $subImage
     * @return Kitchen
     */
    public function addSubImage(\PWD\WebsiteBundle\Entity\KitchenSubImage $subImage)
    {
        $this->subImage[] = $subImage;
        $subImage->setKitchen($this); # used for persisting

        return $this;
    }

    /**
     * Remove subImage
     *
     * @param \PWD\WebsiteBundle\Entity\KitchenSubImage $subImage
     */
    public function removeSubImage(\PWD\WebsiteBundle\Entity\KitchenSubImage $subImage)
    {
        $this->subImage->removeElement($subImage);
    }
}

KitchenSubImage Entity

<?php

namespace PWD\WebsiteBundle\Entity;

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

/**
 * @ORM\Entity
 * @ORM\Table(name="kitchenImages")
 */
class KitchenSubImage 
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @Assert\Image(
     *     minWidth = 800,
     *     maxWidth = 800,
     *     minHeight = 467,
     *     maxHeight = 467
     * )
     */
    public $image;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    public $imagePath;

    /**
     * @ORM\ManyToOne(targetEntity="Kitchen", inversedBy="subImage")
     * @ORM\JoinColumn(name="kitchen_id", referencedColumnName="id")
     **/
    protected $kitchen;

    public function getImage()
    {
        return $this->image;
    }

    public function setImage(UploadedFile $image = null)
    {
        $this->image = $image;
    }

    public function getAbsolutePath()
    {
        return null === $this->imagePath
            ? null
            : $this->getUploadRootDir().'/'.$this->imagePath;
    }

    public function getWebPath()
    {
        return null === $this->imagePath
            ? null
            : $this->getUploadDir().'/'.$this->imagePath;
    }

    public function getUploadRootDir()
    {
        return __DIR__.'/../../../../web/'.$this->getUploadDir();
    }

    public function getUploadDir()
    {
        return 'uploads/our-work';
    }

    public function upload()
    {
        if (null === $this->getImage()) {
            return;
        }

        $this->getImage()->move(
            $this->getUploadRootDir(),
            $this->getImage()->getClientOriginalName()
        );

        $this->mainImagePath = $this->getImage()->getClientOriginalName();

        $this->mainImage = null;
    }

    /**
     * Set imagePath
     *
     * @param string $imagePath
     * @return KitchenSubImage
     */
    public function setImagePath($imagePath)
    {
        $this->imagePath = $imagePath;

        return $this;
    }

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

    /**
     * Set kitchen
     *
     * @param \PWD\WebsiteBundle\Entity\Kitchen $kitchen
     * @return KitchenSubImage
     */
    public function setKitchen(\PWD\WebsiteBundle\Entity\Kitchen $kitchen = null)
    {
        $this->kitchen = $kitchen;

        return $this;
    }

    /**
     * Get kitchen
     *
     * @return \PWD\WebsiteBundle\Entity\Kitchen 
     */
    public function getKitchen()
    {
        return $this->kitchen;
    }

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

KitchenType:

<?php 

namespace PWD\AdminBundle\Form\Type;

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

class KitchenType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('name');
        $builder->add('description', 'textarea');
        $builder->add('mainImage', 'file');
        $builder->add('subImage', 'collection', array(
            'type' => new KitchenSubImageType(), 
            'label' => false,
            'allow_add' => true, 
            'by_reference' => false,
        ));
        $builder->add('submit', 'submit');
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'PWD\WebsiteBundle\Entity\Kitchen',
            'cascade_validation' => true,
        ));
    }

    public function getName()
    {
        return 'kitchen';
    }
}

KitchenSubImageType:

<?php 

namespace PWD\AdminBundle\Form\Type;

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

class KitchenSubImageType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('image', 'file', array('label' => 'Sub Images'));
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'PWD\WebsiteBundle\Entity\KitchenSubImage',
        ));
    }

    public function getName()
    {
        return 'kitchensubimage';
    }
}

Upvotes: 1

Views: 1313

Answers (1)

Cerad
Cerad

Reputation: 48865

Welcome back. Kind of wish that you had taken my previous suggestion and gone though the blog/tags example. You are still having big picture issues with collections.

In your kitchen entity, this is all wrong:

protected $subImage;

public function getSubImage()
{
    return $this->subImage;
}
public function setSubImage(KitchenSubImage $subImage = null)
{
    $this->subImage = $subImage;
}

It should be:

protected $subImages;

public function getSubImages()
{
    return $this->subImages;
}
public function addSubImage(KitchenSubImage $subImage)
{
    $this->subImages[] = $subImage;
    $subImage->setKitchen($this);
}

See how a collection aka relation works in Doctrine?. Just like the bolg/tags example shows. As the form component processes the subImages collection, it will call addSubImage for each posted KitchenSubImage.

The above change may or may not fix everything. Kind of doubt it. If not:

Please tell me that you got the Kitchen form working before you added the sub image collection? You are able to load/store/retrieve the main image? If not, comment out $builder->add('subImage', 'collection', and focus on the kitchen entity.

Once kitchen is working, add subImages back into the form but comment out the allow_add and report what happens.

===================================================

With respect to how the sub images are processed, I can understand some of the confusion. I have not implemented a collection of images my self. Might be some gotchas.

I do know that your need to call upload on each sub image. upload is actually a somewhat misleading name. The file is already on your serve sitting in a tmp directory somewhere. Upload just moves it to a permanent location and stores the path in your entity.

Start by trying this:

    if ($form->isValid()) {
        $kitchen->upload();
        foreach($kitchen->getSubImages() as $subImage)
        {
            $subImage->upload();
        }
        // really should redirect here but okay for now
        return $this->render('PWDWebsiteBundle:Pages:home.html.twig');
    }

It might be better to loop on subImages in kitchen::upload but try it in the controller for now.

Upvotes: 1

Related Questions