Symfony / Sonata: Add a clone function in a CRUD

To add an object duplication feature to a CRUD list, you need to modify the list by adding the button, configure a route, execute object duplication code, and finally, reference the controller that will host our function in our interface, via its service.

Adding the button:
In your controller, add the button by referencing the button template.
Here we set the path 'Admin/list__action_clone.html.twig'. The system will look for the file at: /templates/Admin/list__action_clone.html.twig

   protected function configureListFields(ListMapper $listMapper)
    {

        $listMapper->addIdentifier('id', null, ['label' => 'id']);
        $listMapper->addIdentifier('label', null, ['label' => 'type']);  
        $listMapper->addIdentifier('description', null, ['label' => 'Label']);
        $listMapper->add('actif', null, ['editable' => true]);
        $listMapper->add('_action', null, [
				'actions' => [
					'clone' => [
						'template' => 'Admin/list__action_clone.html.twig'
					]
				]
			]);
        
    }

The content of our twig file is very simple. It's just the display of an icon.

<a  class="btn btn-sm btn-default" 
    href="{{ admin.generateObjectUrl('clone', object) }}" 
    title="{{ 'Clone'|trans({}, 'default') }}" 
    alt="{{ 'Clone'|trans({}, 'default') }}">
	<i class="fa fa-clone"></i>
</a>

The next step is to create the route in our controller:

    protected function configureRoutes(RouteCollection $collection)
    {
        $collection->add('clone', $this->getRouterIdParameter().'/clone');
    }

And the last step, clone our object along with all of its configurations. To do this, we will create a controller specifically dedicated to managing our additional functions and add it to our service so it becomes available.

In App/Admin we're going to create a file called CustomAction.php which will contain our function cloneAction().
This must be extended by CRUDController and not AbstractAdmin as with our interfaces. This is why we put our function in a new file. But it also allows us to reuse this function if needed.


In src/Admin/CustomAction.php:

<?php
namespace App\Admin;

use Sonata\AdminBundle\Controller\CRUDController;
use Symfony\Component\HttpFoundation\RedirectResponse;

class CustomAction extends CRUDController
{
    
    
    /**
     * @param $id
     */
    public function cloneAction($id)
    {
        $object = $this->admin->getObject($id);
        $objectConf=$object->getWtypeWconf()->toArray();
    
    
        if (!$object) {
            throw new NotFoundHttpException(sprintf('unable to find the object with id: %s', $id));
        }
    
        $clonedObject = clone $object;
        $clonedObject->unsetId();
        $clonedObject->setLabel($object->getLabel().' clone('.uniqid().')');
        $this->admin->create($clonedObject);
        if(!empty($objectConf)){
            foreach($objectConf as $c){
                $clonedConf=clone $c;
                $clonedConf->setWconfTypcont($clonedObject);
                $this->admin->create($clonedConf);
            }
        }
        $this->addFlash('sonata_flash_success', 'L\'élément a correctement été dupliqué.');
        return new RedirectResponse($this->admin->generateUrl('list'));
    
        // if you have a filtered list and want to keep your filters after the redirect
        // return new RedirectResponse($this->admin->generateUrl('list', ['filter' => $this->admin->getFilterParameters()]));
    }
}

As you can see, our clone function is responsible for cloning our object as well as all of its OneToMany relationships.
The line $clonedConf->setWconfTypcont($clonedObject); is used to put the id of the parent relationship in the field of the child relationship. Otherwise, all objects would keep their relationships with the initial object.

Last step, we need to reference our new controller in the service of our interface.

Before:

admin.wtype:
    class: App\Admin\WtypeAdmin
    arguments: [~, App\Entity\Wtype, ~]
    calls:
        - [addChild, ["@admin.wconf"]] 
    tags:
        - { name: sonata.admin, manager_type: orm, label: "Types de contrats" }
    public: true

After:

    admin.wtype:
        class: App\Admin\WtypeAdmin
        arguments: [~, App\Entity\Wtype, App\Admin\CustomAction]
        calls:
            - [addChild, ["@admin.wconf"]] 
        tags:
            - { name: sonata.admin, manager_type: orm, label: "Types de contrats" }
        public: true