Symfony2, File upload - delete old and create new in edit

15,733

Solution 1

So I found solution. For first, I had to create an Assert callback for File Uploading, because I was using NotNull() Assert for Reference entity. So if I selected any file and sent form, I was always getting error You have to choose a file. So my first edit was here:

use Symfony\Component\Validator\ExecutionContextInterface;      // <-- here

/**
 * @ORM\Entity(repositoryClass="Acme\ReferenceBundle\Entity\ReferenceRepository")
 * @ORM\Table(name="`references`") 
 * @ORM\HasLifecycleCallbacks 
 * @Assert\Callback(methods={"isFileUploadedOrExists"})       <--- and here
 */
class Reference
{
    // code
}

and then in my code add a new method:

public function isFileUploadedOrExists(ExecutionContextInterface $context)
{
    if(null === $this->image_path && null === $this->file)
        $context->addViolationAt('file', 'You have to choose a file', array(), null);   
}

Also I deleted NotNull assertion in my $image_path property.

Then it was working successfuly - if I selected a file and submitted the form, reference was created with image. But it wasn't finished yet. There was my problem which I asked in this question - delete old image and create a new image with new path, of course.

After many experiments, i found the working and good looking solution. In my controller, I added one variable before form validation and after it is used to delete old image:

$oldImagePath = $reference->getImagePath(); // get path of old image

if($form->isValid())
{
    if ($form->get('file')->getData() !== null) { // if any file was updated
        $file = $form->get('file')->getData();
        $reference->removeFile($oldImagePath); // remove old file, see this at the bottom
        $reference->setImagePath($file->getClientOriginalName()); // set Image Path because preUpload and upload method will not be called if any doctrine entity will not be changed. It tooks me long time to learn it too.
    }
    $em->persist($reference);
    try {
        $em->flush();
    } catch (\PDOException $e) {
        //sth
    }

And my removeFile() method:

public function removeFile($file)
{
    $file_path = $this->getUploadRootDir().'/'.$file;
    if(file_exists($file_path)) unlink($file_path);
}

And at the end, I deleted $this->image_path = $this->file->getClientOriginalName(); line in upload() method because it causes a problem with preview image in the form, if you use any. It sets an original file name as path, but if you reload page, you will see the real path of image. Removing this line will fix the problem.

Thanks everyone to posting answers, who helps me to find the solution.

Solution 2

If the image_path is already set there is an "old" image you want to replace.

Inside your upload() method instead of ...

    // set the path property to the filename where you've saved the file
    $this->image_path = $this->file->getClientOriginalName();

... check for existance of a previous file and remove it before:

 if ($this->image_path) {
     if ($file = $this->getAbsolutePath()) {
        unlink($file);
    }
 }
 $this->image_path = $this->file->getClientOriginalName();

Solution 3

the @Assert\NotNull on the image_path property is tested before your PrePersist/PreUpdate method, so the form validation is not happy because image_path is only provided in the entity internal, the request does not provide the form with "image_path" property, I think you should remove this Assert which is not really useful I think since it is not linked to a form.

OR

your old image_path is the fresh one, and not the old one because it is processed after form binding.

Solution 4

You should use event listeners, which are way better then annotation events in entities, so that you will be able in preUpdate event to retrieve the right values.

You could the use methods like these:

hasChangedField($fieldName) to check if the given field name of the current entity changed.
getOldValue($fieldName) and getNewValue($fieldName) to access the values of a field.
setNewValue($fieldName, $value) to change the value of a field to be updated.
Share:
15,733
Lkopo
Author by

Lkopo

Updated on June 13, 2022

Comments

  • Lkopo
    Lkopo almost 2 years

    I have working entity References.php including Image, but I don't know how to in Symfony2 delete old image saved in this reference (if exists) and create new. Because now, it didn't delete current image, so only created a new and set new image_path into this entity. Here is my try to delete it on preUpload method but it set current file to NULL and then nothing (so I have error - You have to choose a file)

    <?php
    
    namespace Acme\ReferenceBundle\Entity;
    
    use Doctrine\ORM\Mapping as ORM;
    use Symfony\Component\Validator\Constraints as Assert;
    use Symfony\Component\HttpFoundation\File\UploadedFile;
    
    /**
     * @ORM\Entity(repositoryClass="Acme\ReferenceBundle\Entity\ReferenceRepository")
     * @ORM\Table(name="`references`") 
     * @ORM\HasLifecycleCallbacks 
     */
    class Reference
    {
        /**
         * @ORM\Id
         * @ORM\Column(type="integer")
         * @ORM\GeneratedValue(strategy="AUTO")
         */
        private $id;              
    
        /**
         * @ORM\Column(type="string", length=200)    
         * @Assert\NotBlank(
         *      message = "Name cannot be blank"      
         * )    
         * @Assert\Length(
         *      min = "3",
         *      minMessage = "Name is too short"         
         * )     
         */     
        private $name;
    
        /**
         * @ORM\Column(type="string", length=200)    
         * @Assert\NotBlank(
         *      message = "Description cannot be blank"      
         * )    
         * @Assert\Length(
         *      min = "3",
         *      minMessage = "Description is too short"         
         * )     
         */     
        private $description;
    
        /**
         * @ORM\Column(type="string", length=200)
         * @Assert\Url(
         *      message = "URL is not valid"
         * )          
         */     
        private $url;
    
        /**
         * @ORM\ManyToMany(targetEntity="Material", inversedBy="references")
         * @Assert\Count(min = 1, minMessage = "Choose any material") 
         */
        private $materials;
    
        /**
         * @ORM\Column(type="text", length=255, nullable=false)
         * @Assert\NotNull(
         *      message = "You have to choose a file"
         * )     
         */
        private $image_path;
    
        /**
         * @Assert\File(
         *     maxSize = "5M",
         *     mimeTypes = {"image/jpeg", "image/gif", "image/png", "image/tiff"},
         *     maxSizeMessage = "Max size of file is 5MB.",
         *     mimeTypesMessage = "There are only allowed jpeg, gif, png and tiff images"
         * )
         */
        private $file;                  
    
        /**
         * Get id
         *
         * @return integer 
         */
        public function getId()
        {
            return $this->id;
        }
    
        /**
         * Set name
         *
         * @param string $name
         * @return Reference
         */
        public function setName($name)
        {
            $this->name = $name;
    
            return $this;
        }
    
        /**
         * Get name
         *
         * @return string 
         */
        public function getName()
        {
            return $this->name;
        }
    
        /**
         * Set description
         *
         * @param string $description
         * @return Reference
         */
        public function setDescription($description)
        {
            $this->description = $description;
    
            return $this;
        }
    
        /**
         * Get description
         *
         * @return string 
         */
        public function getDescription()
        {
            return $this->description;
        }
    
        /**
         * Set url
         *
         * @param string $url
         * @return Reference
         */
        public function setUrl($url)
        {
            $this->url = $url;
    
            return $this;
        }
    
        /**
         * Get url
         *
         * @return string 
         */
        public function getUrl()
        {
            return $this->url;
        }
    
        /**
         * Set materials
         *
         * @param string $materials
         * @return Reference
         */
        public function setMaterials($materials)
        {
            $this->materials = $materials;
    
            return $this;
        }
    
        /**
         * Get materials
         *
         * @return string 
         */
        public function getMaterials()
        {
            return $this->materials;
        }
    
        /**
         * Set image_path
         *
         * @param string $imagePath
         * @return Reference
         */
        public function setImagePath($imagePath)
        {
            $this->image_path = $imagePath;
    
            return $this;
        }
    
        /**
         * Get image_path
         *
         * @return string 
         */
        public function getImagePath()
        {
            return $this->image_path;
        }
    
        public function setFile(UploadedFile $file = null)
        {
            $this->file = $file;
        }
    
        /**
         * Get file.
         *
         * @return UploadedFile
         */
        public function getFile()
        {
            return $this->file;
        }
    
        /**
        * Called before saving the entity
        * 
        * @ORM\PrePersist()
        * @ORM\PreUpdate()
        */
        public function preUpload()
        {   
            $oldImage = $this->image_path;
            $oldImagePath = $this->getUploadRootDir().'/'.$oldImage;
    
            if (null !== $this->file) {
                if($oldImage && file_exists($oldImagePath)) unlink($oldImagePath); // not working correctly
                $filename = sha1(uniqid(mt_rand(), true));
                $this->image_path = $filename.'.'.$this->file->guessExtension();
            }
        }
    
        /**
        * Called before entity removal
        *
        * @ORM\PostRemove()
        */
        public function removeUpload()
        {
            if ($file = $this->getAbsolutePath()) {
                unlink($file);
            }
        }
    
        /**
        * Called after entity persistence
        *
        * @ORM\PostPersist()
        * @ORM\PostUpdate()
        */
        public function upload()
        {
            // the file property can be empty if the field is not required
            if (null === $this->file) {
                return;
            }
    
            // use the original file name here but you should
            // sanitize it at least to avoid any security issues
    
            // move takes the target directory and then the
            // target filename to move to
            $this->file->move(
                $this->getUploadRootDir(),
                $this->image_path
            );
    
            // set the path property to the filename where you've saved the file
            $this->image_path = $this->file->getClientOriginalName();
    
            // clean up the file property as you won't need it anymore
            $this->file = null;
        }
    
        protected function getAbsolutePath()
        {
            return null === $this->image_path
                ? null
                : $this->getUploadRootDir().'/'.$this->image_path;
        }
    
        protected function getUploadRootDir()
        {
            // the absolute directory path where uploaded
            // documents should be saved
            return __DIR__.'/../../../../'.$this->getUploadDir();
        }
    
        protected function getUploadDir()
        {
            // get rid of the __DIR__ so it doesn't screw up
            // when displaying uploaded doc/image in the view.
            return 'uploads/references';
        }
    
        public function getWebPath()
        {
            return $this->getUploadDir().'/'.$this->image_path;
        }
        /**
         * Constructor
         */
        public function __construct()
        {
            $this->materials = new \Doctrine\Common\Collections\ArrayCollection();
        }
    
        /**
         * Add materials
         *
         * @param \Acme\ReferenceBundle\Entity\Material $materials
         * @return Reference
         */
        public function addMaterial(\Acme\ReferenceBundle\Entity\Material $materials)
        {
            $this->materials[] = $materials;
    
            return $this;
        }
    
        /**
         * Remove materials
         *
         * @param \Acme\ReferenceBundle\Entity\Material $materials
         */
        public function removeMaterial(\Acme\ReferenceBundle\Entity\Material $materials)
        {
            $this->materials->removeElement($materials);
        }
    }
    

    Any idea?

  • Lkopo
    Lkopo over 10 years
    It's useful, because without it if someone try to post new form without uploaded image, he will get a MySQL Exception, which is not very pretty.
  • Lkopo
    Lkopo over 10 years
    So, how I can get the old Image path?
  • Taytay
    Taytay over 10 years
    I would try to get it before calling form->handleRequest or form->bind, with a different method in the Entity, so after binding, the entity is clean
  • Lkopo
    Lkopo over 10 years
    But also, I don't want to remove old Image if form is not validated successfuly
  • Taytay
    Taytay over 10 years
    Do not remove the image before validation, just store it somewhere in case, and call entity->deleteOldImageIfExists() only if form is valid
  • Lkopo
    Lkopo over 10 years
    So there is maybe one way. But it looks more uselessly difficult. Here (stackoverflow.com/questions/10498807/…) I found deleting images in upload method, but not working good.
  • Taytay
    Taytay over 10 years
    seems not automatic to me, I prefer the automatic solution
  • Taytay
    Taytay over 10 years
    DO you use image_path in your form or file property ?
  • Lkopo
    Lkopo over 10 years
    Still on creating a new reference message (You have to choose a file) if I select an image to upload.
  • Lkopo
    Lkopo over 10 years
    I'm not sure what are you asking for
  • Taytay
    Taytay over 10 years
    have you tried getting the image_path name from the entity before bind , and then if isValid delete the old file using the path you got before if it exists ?