Home » Php » php – Best practices – Delete links with Symfony 2

php – Best practices – Delete links with Symfony 2

Posted by: admin July 12, 2020 Leave a comment

Questions:

In Symfony 2, which is the best way to create a link to delete a record?

I can define a route to /entity/delete accepting only a DELETE method, but I don’t know how to create a DELETE link from a template. The same goes for creating PUT links.

So, what do you do? Accept GET petition to delete a record? Is there any way to create a DELETE link?

How to&Answers:

Anchors can fire only GET requests. And since GET requests can be cached and in some future (when you implement serious caching) might not reach your app code at all, it’s a bad practice to use GET for changing anything that modifies state of your App. Here is what I do in my projects. With these two, you will be able to just do the following in your templates:

<a href="{{ path('my_entity_destroy') }}" {{ delete_link(myEntity) }}>Delete</a>

How it works

Assuming your entity delete path is: /my_entity, methods: [DELETE]

Principle is really simple. delete_link extension method will create data-attribute on anchor, so compiled anchor will look like this:

<a href="/my_entity" data-delete-link="3964">Delete</a>

Then, when somebody clicks on that link, javascript will catch that click, create a form and fire a DELETE request with id provided in data-delete-link attribute.

What makes it work

Here is what makes it possible, a LinkHelper Twig extension:

<?php

namespace Me\MyBundle\Twig;

class LinkHelperExtension extends \Twig_Extension
{
    public function getFunctions()
    {
        return [
            new \Twig_SimpleFunction('delete_link', [$this, 'fnDeleteLink'], ['is_safe' => ['all']]),
        ];
    }

    public function fnDeleteLink($target)
    {
        if (is_object($target)) {
            $target = $target->getId();
        }
        return "data-delete-link='$target'";
    }

    public function getName()
    {
        return 'link_helper';
    }
} 

And I a JavaScript in my base template:

$(function () {
    var createForm = function (action, data) {
        var $form = $('<form action="' + action + '" method="POST"></form>');
        for (input in data) {
            if (data.hasOwnProperty(input)) {
                $form.append('<input name="' + input + '" value="' + data[input] + '">');
            }
        }

        return $form;
    };

    $(document).on('click', 'a[data-delete-link]', function (e) {
        e.preventDefault();
        var $this = $(this);

        var $form = createForm($this.attr('href'), {
            id: $this.attr('data-delete-link'),
            _method: 'DELETE'
        }).hide();

        $('body').append($form); // Firefox requires form to be on the page to allow submission
        $form.submit();
    });
});

Limitations

It only works for entities that have primary key named id. However, you can pretty easy modify this to suit your needs and support composite primary keys etc.

Answer:

Symfony2 _method functionality description can be found in How to Define Route Requirements. Below is the solution I use.

Link

<a
    href="{{ path('my_delete_route_name', {'id': some_entity.id}) }}"
    class="as-form"
    data-method="delete"
    data-csrf="_token:{{ csrf }}"
>{{ 'delete'|trans({}, 'button') }}</a>

Re-Format the above link to inline after copy-paste

Attach onClick event listener:

JS

$('.as-form').on('click',function(){
    var $form = $('<form/>').hide();

    //form options
    $form.attr({
        'action' : $(this).attr('href'),
        'method':'post'
    })

    //adding the _method hidden field
    $form.append($('<input/>',{
        type:'hidden',
        name:'_method'
    }).val($(this).data('method')));

    //adding a CSRF if needs
    if ($(this).data('csrf'))
    {
        var csrf = $(this).data('csrf').split(':');
        $form.append($('<input/>',{
            type:'hidden',
            name:csrf[0]
        }).val(csrf[1]));
    }

    //add form to parent node
    $(this).parent().append($form);

    $form.submit();

    return false;
});

Controller

class MyCustomController extends Controller
{
    /**
    * @Route("/delete/{id}",name="my_delete_route_name")
    * @Method("DELETE")
    * @ParamConverter("entity", class="MyEntityClass")
    * @CsrfProtector(intention="my_csrf_intention", name="_token")
    */
    public function deleteAction(Request $request, $entity)
    {
        // do whatever you need
    }
}

Note

@CsrfProtector is my custom annotation that is used to validate the CSRF token passed in request before running the controller method.

Answer:

These solutions don’t work without JavaScript enabled. If you just need a “Delete Button” in your Edit-Form it would be also possible to create a second submit with delete name and redirect in the updateAction in your Controller to the deleteAction if the “delete-submit” was clicked.

Answer:

Based on the advice of Bernhard Zürn, see how to do delete link without javascript :

1) In your form builder, add a second submit button.

$builder
    // ... [other fields] ...

    ->add('save', SubmitType::class, array(
        'attr'      => array('class' => 'button-link save'),
        'label'     => 'Validate'
    ))
    ->add('delete', SubmitType::class, array(
        'attr'      => array('class' => 'button-link delete'),
        'label'     => 'Delete'
    ));

2) In your twig template, show the delete button wherever you want. In this example, I use the same form to add and edit a user. So, I put the code in an ‘if’ condition to detect edit action.

{% if user_id is defined %}
    {{ form_widget(form.delete) }}
{% endif %}

3) In your controller, use the button’s isClicked() method for querying if the “delete” button was clicked:

   /**
    * @Route("/users/edit/{id}", name="user_edit")
    */
   public function editAction($id, Request $request) {    
      // get user from database
      $em = $this->getDoctrine()->getManager();
      $user = $em->getRepository('AppBundle:User')->find($id);

      // user doesn't exist
      if (!$user) {
         throw $this->createNotFoundException('No user found for id '. $id);
      }

      // build the form with user data
      $form = $this->createForm(UserType::class, $user);

      // form POST
      $form->handleRequest($request);
      if ($form->isSubmitted() && $form->isValid()) {

         /*** SAVE ***/
         if ($form->get('save')->isClicked()) {
           // ... perform some action    

         /*** DELETE ***/
         } elseif ($form->get('delete')->isClicked()) {
            $em->remove($user);
            $em->flush();

            // message
            $this->addFlash('notice', "User has been deleted successfully !");

            // show list
            return $this->redirectToRoute('user');
         }
      }

      // show form
      return $this->render('users/form.html.twig', array(
         'form'    => $form->createView(),
         'user_id' => $id
      ));
   }

To make further progress, you can add a confirm message Are you sure you want to delete this user ? in javascript before executing route action.