phpsymfonycachinglistenerliipimaginebundle

Symfony 4 - LiipImagine - Listener delete all my directory (cache clear listener)


I have a symfony 4 project with a User entity that has a relationship with an Avatar entity (images uploaded with VichUploaderBundle).

In Avatar.php:

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

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

In User.php:

/**
 * @ORM\OneToOne(targetEntity="App\Entity\Avatar", mappedBy="user", cascade={"persist", "remove"})
 */
private $avatar;

I have a profile page to edit a user's data (name, surname, mail, avatar). In this page, I use LiipImagineBundle to display the current avatar in a certain dimension.

When the user edits his profile, I wish a listener can check is there are changes in the avatar. In which case, it deletes the old media / cache.

So I created a Listener for that:

<?php

namespace App\Listener;

use App\Entity\Avatar;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Liip\ImagineBundle\Imagine\Cache\CacheManager;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Vich\UploaderBundle\Templating\Helper\UploaderHelper;

class ImageCacheSubscriber implements EventSubscriber
{

    /**
     * CacheManager
     *
     * @var CacheManager
     */
    private $cacheManager;

    /**
     * UploaderHelper
     *
     * @var UploaderHelper
     */
    private $uploaderHelper;

    public function __construct(CacheManager $cacheManager, UploaderHelper $uploaderHelper)
    {
        $this->cacheManager = $cacheManager;
        $this->uploaderHelper = $uploaderHelper;
    }

    public function getSubscribedEvents()
    {
        return [
            'preRemove',
            'preUpdate'
        ];
    }

    public function preRemove(LifecycleEventArgs $args)
    {

        $entity = $args->getEntity();
        if (!$entity instanceof Avatar) {
            return;
        }

        $this->cacheManager->remove($this->uploaderHelper->asset($entity, 'imageFile'));
    }

    public function preUpdate(PreUpdateEventArgs $args)
    {
        dump($args->getEntity());
        dump($args->getObject());

        $entity = $args->getEntity();
        if (!$entity instanceof Avatar) {
            return;
        }

        if ($entity->getImageFile() instanceof UploadedFile) {
            $this->cacheManager->remove($this->uploaderHelper->asset($entity, 'imageFile'));
        }
    }
}

Services.yaml:

  App\Listener\ImageCacheSubscriber:
    tags:
      - { name: doctrine.event_subscriber }

But when I change my avatar, the listener removes the entire folder containing the avatars in the media.

And he provokes me this error:

Failed to remove directory "C:\Users\user\Desktop\congesTest2/public/media/cache/avatar_big\files": rmdir(C:\Users\user\Desktop\congesTest2/public/media/cache/avatar_big\files): Directory not empty.

I don't understand why ... :'(

EDIT:

I update my function preUpdate() to postUpdate() :

public function getSubscribedEvents()
    {
        return [
            'preRemove',
            'postUpdate'
        ];
    }
public function postUpdate(LifecycleEventArgs $args)
    {
        dump($args->getEntity());

        $entity = $args->getEntity();
        if (!$entity instanceof Avatar) {
            return;
        }

        if ($entity->getImageFile() instanceof UploadedFile) {
            $this->cacheManager->remove($this->uploaderHelper->asset($entity, 'imageFile'));
        }
    }

And now, if I make a dump of :

    dd($this->uploaderHelper->asset($entity, 'imageFile'));

I've :

"/images/avatar/avatar3.jpg" And this is the good path ! On the other hand, the image is not removed from the cache! The remove () function does not seem to give anything it's amazing

With the dump of the entity, I saw that the file was no longer an UploadedFile but a File simply. Whereas before it seemed like an UploadedFile. So I changed the line

if ($entity->getImageFile() instanceof UploadedFile)

by

if ($entity->getImageFile() instanceof File)

But the image is still not deleted from the cache.

In my opinion, since this is a postUpdate, it removes the new cache image, not the old one. But since the user is redirected to the same page, he delivers it immediately after caching. (EDIT : No I did a test, the image is not even removed from the cache)


Solution

  • Instead of listening to Doctrine events, you can listen to the vich_uploader.pre_remove event. This will ensure you get the old image that needs to be removed every time. First, make sure your VichUploader config is set to delete files on update and remove. This is the default.

    # config/packages/vich_uploader.yaml
    
    vich_uploader:
        mappings:
            avatar:
                upload_destination: '%kernel.project_dir%/public/uploads/avatar'
                uri_prefix: 'uploads/avatar'
    
                delete_on_update: true
                delete_on_remove: true
    

    Now you need to create the listener.

    // src/EventListener/ImageCacheSubscriber.php
    
    namespace App\EventListener;
    
    use Liip\ImagineBundle\Imagine\Cache\CacheManager;
    use Symfony\Component\EventDispatcher\EventSubscriberInterface;
    use Vich\UploaderBundle\Event\Event;
    use Vich\UploaderBundle\Event\Events;
    use Vich\UploaderBundle\Storage\StorageInterface;
    
    class ImageCacheSubscriber implements EventSubscriberInterface
    {
        private $storage;
        private $cacheManager;
    
        public function __construct(StorageInterface $storage, CacheManager $cacheManager)
        {
            $this->storage = $storage;
            $this->cacheManager = $cacheManager;
        }
    
        public function onRemove(Event $event)
        {
            $path = $this->storage->resolveUri($event->getObject(), $event->getMapping()->getFilePropertyName());
            $this->cacheManager->remove($path);
        }
    
        public static function getSubscribedEvents()
        {
            return [Events::PRE_REMOVE => 'onRemove'];
        }
    }
    

    When any VichUploader asset is removed, this listener will attempt to remove it from the cache for all filters. You can specify specific filters in the CacheManager::remove() method if you would like. You could also only remove the cache for specific entities by checking the instance of $event->getObject().

    This also makes a few assumptions about your LiipImagine config. If you're using the default loader and cache resolver, this should work. If you're using a custom loader or resolver, you may need to modify this listener to your needs.

    # config/packages/liip_imagine.yaml
    
    liip_imagine:
        resolvers:
            default:
                web_path:
                    web_root: '%kernel.project_dir%/public'
                    cache_prefix: 'media/cache'
    
        loaders:
            default:
                filesystem:
                    data_root:
                        - '%kernel.project_dir%/public'
    
        filter_sets:
            cache: ~
    
            # Your filters...
    

    If you're using Symfony Flex, you're done. Otherwise, make sure to register the listener as a service.

    # config/services.yaml
    
    services:
        # ...
        App\EventListener\ImageCacheSubscriber:
            arguments: ['@vich_uploader.storage.file_system', '@liip_imagine.cache.manager']
            tags:
                - { name: kernel.event_subscriber }