The Trick to Getting Gedmo Uploadable working with Sonata Admin

Recently I needed to add file upload capabilities to an existing Symfony project and decided to use Gedmo’s Doctrine2 Behavioral Extensions. Gedmo adds a rich array of functionality to Doctrine Entities such as the ability to timestamp a table (timestampable), or record the last user to edit a record (blameable), but what I needed was to give my users the ability to upload a simple profile pic. So I started with uploadable. My goal was to allow a user to upload an image through a Sonata Admin interface and have the path to that image stored in a “img_path” column. I spent hours pouring over the documentation but after my best attempt all I got was the name of a PHP temporary uploaded file, but no the actual file. Fast forward many frustrating hours and I have a solution which will hopefully save someone some time.

The documentation that I read kept referring to some sort of listener. In retrospect it all makes perfect sense. I could have created an uploadable listener service and injected that into my controller and all probably would have been dandy. But #1, I wasn’t using typical controllers, I’m using Sonata Admin classes and #2 I like to make things difficult! Instead of following the documentation I ended up discovering a helper package in the StofDoctrineExtensionsBundle. This bundle basically sets up Symfony2-friendly listener services for all Gedmo Doctrine2 Extensions. “Wonderful!”, I thought. But 2 hours later, I still had not achieved my goal. Here’s what I did:

First I updated my composer.json to include StofDoctrineExtensions.

`

   "require": {
       "stof/doctrine-extensions-bundle": "1.1.*@dev"
}

`

This not only pulled in Stof’s bundle, but also the Gedmo Doctrine Extensions themselves. Next, I added the newly added bundle to my app/AppKernel.php.

    new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle()

Next I added the following configuration to my app/config/config.yml to enable the uploadable listener.

  stof_doctrine_extensions:
      default_locale: en_US
      uploadable:
          # Default file path: This is one of the three ways you can configure the path for the Uploadable extension
          default_file_path: %kernel.root_dir%/../web/uploads
               
          # Mime type guesser class: Optional. By default, we provide an adapter for the one present in the HttpFoundation component of Symfony
          mime_type_guesser_class: Stof\DoctrineExtensionsBundle\Uploadable\MimeTypeGuesserAdapter
                
          # Default file info class implementing FileInfoInterface: Optional. By default we provide a class which is prepared to receive an UploadedFile instance.
          default_file_info_class: Stof\DoctrineExtensionsBundle\Uploadable\UploadedFileInfo 
  
      orm:
          default:
             uploadable: true

And then I updated my Entity to include Gedmo annotations for an uploadable column.

<?php
  use Gedmo\Mapping\Annotation\Uploadable;

  
  /**
   * Contact
   *
   * @ORM\Table()
   * @Gedmo\Mapping\Annotation\Uploadable(allowOverwrite=true, filenameGenerator="SHA1")
   */
  class Contact
  {
      ...

       /**
       * @var string
       * @ORM\Column(name="img_path", type="string", length=255, nullable=true)
       * @Gedmo\Mapping\Annotation\UploadableFilePath 
       */
      private $imgPath;

     ...
   }

This basically states that I wanted any image uploaded to this entity to have its path stored in a column called “img_path”. (Of course I created a Doctrine Migration to add this column to my db.)
Finally, I added a file form input to my contact form so that I could actually upload an image.

<?php
use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Form\FormMapper;

class ContactAdmin extends Admin
{ 
     protected function configureFormFields(FormMapper $formMapper)
     {   
          $formMapper
              ->add('first_name')
              ->add('last_name')
              ->add('imgPath', 'file', array('required' => false, 'data_class' => null, 'mapped' => true))
          ;
          ...

With all this done, I tested my contact form and attempted to upload an image. The image seemed to have been uploaded just fine, but the only thing I could manage to get saved in my imgPath column was a PHP temporary file name like /tmp/phpEdfuKh. Frustrating to say the least. What I discovered was that I needed to somehow bind the uploaded image to the entity so that the Gedmo Uploadable extension could kick in and do it’s thing. After doing more reading and trial and error I discovered that I could do this with a simple call to the uploadable manager from within my Admin’s prePersist() method. The only problem was that the only way I could access the uploadable manager was by injecting the @service_container, which is normally a no-no. But for this problem, we made an exception. Here’s what we did:

We used constructor injection to inject the @service_container into the Sonata Admin class. First we had to override the classes constructor.

class ContactAdmin extends Admin
{
    private $container = null;
 
    public function __construct($code, $class, $baseControllerName, $container=null)
    {   
        parent::__construct($code, $class, $baseControllerName);
        $this->container = $container;  
    } 

Then we created a service for it in the bundle’s service.yml.

      tsk.admin.contact:
          class: Acme\DemoBundle\Admin\ContactAdmin
          tags:
              - { name: sonata.admin, manager_type: "orm", group: "Contacts", label: "Contacts" }
~         arguments: [null, Acme\DemoBundle\Entity\Contact, SonataAdminBundle:CRUD, @service_container]
          calls:
              - [ setTranslationDomain, [SonataAdminBundle] ]

Finally, I added a prePersist() method with a call to link the uploaded file to the entity.

    public function prePersist($object)
    {   
        // We get the uploadable manager!
        $uploadableManager = $this->container->get('stof_doctrine_extensions.uploadable.manager');
        $uploadableManager->markEntityToUpload($object, $object->getImgPath());
    }

I’m not crazy about that function name (markEntityToUpload??) but, after reloading my page and adding an image, Fait accompli!!

7 thoughts on “The Trick to Getting Gedmo Uploadable working with Sonata Admin

  1. Triggvy

    Thanks. Exactly what I needed to know.
    BTW: You don’t need to inject the whole service container into your admin class, when you need the upload manager only simply inject @stof_doctrine_extensions.uploadable.manager

    sonata.admin.club:
    class: App\Admin\ClubAdmin
    tags: [ { name: sonata.admin, manager_type: orm, group: "Data", label: "Clubs" } ]
    arguments: [ null, App\Entity\Club, SonataAdminBundle:CRUD, @stof_doctrine_extensions.uploadable.manager ]

    1. burgeris

      Exactly. And why do you say injecting a service (or in this instance – uplodable manager) isn’t acceptable practice?

      The only problem was that the only way I could access the uploadable manager was by injecting the @service_container, which is normally a no-no.

  2. Dan

    Hi I have a question about the three last bit of code. I’m not sure to understand which file you are editing at this stage. I actually still have a temporary path at the end of your tutorial, which by the way is a great idea!!! It’s a week I’m struggling with that now :/ …. yes first timer Ô.ô

    Would be great to have more details.

    Thanks however!

    1. Tamara

      I use a highly mefidiod version of LJXP that suits my particularneeds. Those modifications managed to prevent me from having the erroryou’re mentioning. I’d share it with you but I’m certain the changesmade would not suit you as they are very specific to me and my site.Hopefully the author will update his plugin soon. He has quite a few users.On Wed, Apr 2, 2008 at 10:55 AM, Disqus

  3. guest

    This was very helpful, thank you!
    One addition, the SonataAdmin prePersist hook will only fire for new objects.

  4. Sergey Smirnov

    You can use

    $this->getConfigurationPool()->getContainer()

    to access service container instead injecting it.

  5. npuMumuB

    You could also use setter injection instead of constructor injection to not override the constructor. Like that:


    # App\Admin\ClubAdmin
    class ContactAdmin extends Admin
    {
    /**
    * this one injected by DI
    */
    protected $uploadableManager;
    public function setUploadableManager($uploadableManager){
    $this->uploadableManager = $uploadableManager;
    }
    // ...
    }

    # admin.yml
    services:
    tsk.admin.contact:
    class: Acme\DemoBundle\Admin\ContactAdmin
    tags:
    - { name: sonata.admin, manager_type: orm, group: "Contacts", label: "Contacts" }
    arguments:
    - ~
    - Acme\DemoBundle\Entity\Contact
    - ~
    calls:
    - [ setTranslationDomain, [SonataAdminBundle]]
    - [ setUploadableManager, [@stof_doctrine_extensions.uploadable.manager]]

Comments are closed.