Search code examples
symfonysonata-admin

How to implement many-to-many relationships in Sonata Media Bundle


I am trying to relate SonataMediaBundle to another Entity: Products with a relation ManyToMany.

The schema and relation are well created.

However, when I edit or create a new product, I try to add a button where I can search a media file through the media library and a button to upload a new file.

For a relation OneToMany, this is easily done in Admin\ProductAdmin::configureFormFields by adding:

->add('image', 'sonata_type_model_list', array(
                    'required' => false
                ), array(
                    'link_parameters' => array(
                        'context'  => 'default',
                        'provider' => 'sonata.media.provider.image'
                     )
                ))

So I get the same 3 icons as they already been used in the Gallery of SonataMediaBundle (add from library, upload and delete)

BUT on the relation ManyToMany it isn't possible! Because every time I choose a media, it replaces the previous one. So I can't select multiple media types.

I thought on using the same way as The Gallery (galleryHasMedia)

->add('galleryHasMedias', 'sonata_type_collection', array(
            'by_reference' => false
        ), array(
            'edit'     => 'inline',
            'inline'   => 'table',
            'sortable' => 'position',
            'link_parameters' => array('context' => $context)
        ))

However, it is really complex. How can I choose or upload multiple media files on another Entity through a ManyToMany Relation?


Solution

  • I had the same problem as you, but I've figured it out.

    First of all, you might want to choose for a one-to-many/many-to-one relationship (using an intermediate entity) instead of a many-to-many relationship. Why? Because this allows for additional columns, such as a position column. This way you can reorder the images any way you want. In a many-to-many relationship, the link table only has two columns: the id's of the associated tables.

    From the Doctrine documentation:

    (...) frequently you want to associate additional attributes with an association, in which case you introduce an association class. Consequently, the direct many-to-many association disappears and is replaced by one-to-many/many-to-one associations between the 3 participating classes.

    So I added this to my Product mapping file: (as you can see I'm using YAML as my configuration file format)

    oneToMany:
        images:
            targetEntity: MyBundle\Entity\ProductImage
            mappedBy: product
            orderBy:
                position: ASC
    

    And I created a new ProductImage mapping file:

    MyBundle\Entity\ProductImage:
        type: entity
        table: product_images
        id:
            id:
                type: integer
                generator: { strategy: AUTO }
        fields:
            position:
                type: integer
        manyToOne:
            product:
                targetEntity: MyBundle\Entity\Product
                inversedBy: images
            image:
                targetEntity: Application\Sonata\MediaBundle\Entity\Media
    

    Using the command line (php app/console doctrine:generate:entities MyBundle) I created / updated the corresponding entities (Product and ProductImage).

    Next, I created/updated the Admin classes. ProductAdmin.php:

    class ProductAdmin extends Admin
    {
        protected function configureFormFields(FormMapper $formMapper)
        {
            $formMapper
                // define other form fields
                ->add('images', 'sonata_type_collection', array(
                    'required' => false
                ), array(
                    'edit' => 'inline',
                    'inline' => 'table',
                    'sortable'  => 'position',
                ))
            ;
        }
    

    ProductImageAdmin.php:

    class ProductImageAdmin extends Admin
    {
        protected function configureFormFields(FormMapper $formMapper)
        {
            $formMapper
                ->add('image', 'sonata_type_model_list', array(
                    'required' => false
                ), array(
                    'link_parameters' => array(
                        'context' => 'product_image'
                    )
                ))
                ->add('position', 'hidden')
            ;
        }
    

    Don't forget to add both of them as services. If you don't want a link to the ProductImage form to be displayed on the dashboard, add the show_in_dashboard: false tag. (how you do this depends on the configuration format (yaml/xml/php) you use)

    After this I had the admin form working correctly, however I still had some problems trying to save products. I had to perform the following steps in order to fix all problems:

    First, I had to configure cascade persist operations for the Product entity. Again, how to do this depends on your configuration format. I'm using yaml, so in the images one-to-many relationship, I added the cascade property:

    oneToMany:
        images:
            targetEntity: MyBundle\Entity\ProductImage
            mappedBy: product
            orderBy:
                position: ASC
            cascade: ["persist"]
    

    That got it working (or so I thought), but I noticed that the product_id in the database was set to NULL. I solved this by adding prePersist() and preUpdate() methods to the ProductAdmin class:

    public function prePersist($object)
    {
        foreach ($object->getImages() as $image) {
            $image->setProduct($object);
        }
    }
    
    public function preUpdate($object)
    {
        foreach ($object->getImages() as $image) {
            $image->setProduct($object);
        }
    }
    

    ... and added a single line to the addImages() method of the Product entity:

    public function addImage(\MyBundle\Entity\ProductImage $images)
    {
        $images->setProduct($this);
        $this->images[] = $images;
    
        return $this;
    }
    

    This worked for me, now I can add, change, reorder, delete, etc. images to/from my Products.