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!!