Starting programming with Symfony

At Drupalcamp London I watched a great talk from Richard Miller of Sensio Labs, about treating Symfony as a framework in its own right rather than as a mere substrate to Drupal 8. Which it is, of course; but it's easy for Drupalers to forget that, and I think it would be a shame, after all the efforts of the D8 core team, if it meant we all missed out through complacency on the advantages of a good underlying framework.

You can pretty much go through Richard Miller's slides and use them as instructions to create your own CRUD(i) task list application as you go along. In fact, I suggest you try doing that now. Because I'm about to extend his example below, to include a new menu callback to mark a task as completed.

URL handler in TaskController.php

Add the following URL handling method for marking a task as complete:

    /**
     * Sets a Task entity as completed, or unsets it back as "in progress"
     *
     * @Route("/{id}/complete", name="task_complete")
     * @Method("POST")
     * @ParamConverter()
     * @Template("AcmeListBundle:Task:show.html.twig")
     */
    public function completeAction(Task $task) {
        // Our task will be to either mark as complete (if not yet complete)
        // or to remove that timestamp (reopening, if already complete).
        $marked_as_complete = ($task->getCompleted() === NULL);
        $task->setCompleted($marked_as_complete ? new \DateTime() : NULL);
 
        // Use the entity manager to merge this task into the database.
        $em = $this->getDoctrine()->getManager();
        $em->merge($task);
        $em->flush();
 
        // Hand over to ::showAction(), adding an extra parameter
        // for the template to interrogate.
        return array_merge(
	    $this->showAction($task),
            array('marked_as_complete' => $marked_as_complete)
        );
    }

This will take POST requests to the URL (/task)/[TASK_ID]/complete and use them as a signal to toggle the "completed" date between unset (NULL) and a timestamp of now.

You should spot that:

  • The HTTP method is set to POST using annotations: this is a non-idempotent action - it changes persistent data - and therefore shouldn't use GET.
  • The entity manager must set the task to be merged after the change; but then also needs to be flushed to actually make the writes.
  • The parameter array being returned is a combination of ::showAction() and an extra parameter.
  • The @Template() has to be hardwired, to use ::showAction()'s template rather than look for a complete.html.twig.

All of this gives us the absolute bare minimum callback we need, while reusing as much as possible from elsewhere.

Form submit button to make a POST to this URL

Much like the ::createDeleteForm() form, this callback also needs a form submit button to make the HTTP POST request on being clicked: a normal link to /task/{id}/complete would be rejected by Symfony as having an invalid HTTP method (GET).

We want this form button to be sensitive to the current task's complete/incomplete status, and display a label accordingly; hence it gets a second parameter:

    /**
     * Creates a form to mark a Task entity as complete, by id.
     *
     * @param mixed $id The entity id
     * @param boolean $is_complete Whether the task entity is already complete
     *
     * @return \Symfony\Component\Form\Form The form
     */
    private function createCompleteForm($id, $is_complete = NULL)
    {
        return $this->createFormBuilder()
            ->setAction($this->generateUrl('task_complete', array('id' => $id)))
            ->add('submit', 'submit', array('label' =>
                $is_complete ? 'Re-open as in progress' : 'Mark as complete'))
            ->getForm()
        ;
    }

Injecting this button into the UI

You should then add a call to this method, to any template arrays where delete_form is already set, as per ::createDeleteForm(); and also add the variable in the same way as {{ delete_form }} in the relevant .twig templates. This should display a button, which when clicked toggles the presence of the current datetime as the task's completion timestamp.

Notifying the user of marked-as-complete, as part of the templates

When the template show.html.twig is now called, the parameter marked_as_complete can have three values:

  1. TRUE (task just marked as complete)
  2. FALSE (task just marked as incomplete)
  3. Undefined (task just being displayed on a HTTP GET by other existing handlers, nothing to do with our new one)

We need to provide suitable notifications for the first two options, but also cope with the third option. For that reason, we embed the following logic in the template, just below the h1 title:

    {% if marked_as_complete is defined %}
        {% if marked_as_complete %}
            <p>The task was marked as complete.</p>
        {% else %}
            <p>The task was re-opened.</p>
        {% endif %}
    {% endif %}

This quietly, happily does nothing in the third case. Without the check for is-defined first, Symfony/Twig will raise an error.

And that's it

That should be enough to add a new non-CRUD callback to the example from Drupalcamp London. You can add plenty more if you like, obviously; but I'm doing this as a way of learning Symfony; certainly I've liked what I've seen of it thus far.

Between Symfony, Laravel and other new frameworks - many of which seem to reference each other through Composer anyway! - then PHP is really starting to grow up; in one or other of these frameworks it even arguably has its Django, or its Rails...!