For a software developer position I was asked to make a simple application where user were capable of editing predefined Pizzas and their ingredients (add and delete) , so far the request was quite easy but a third one (order the different ingredients) was a little intimidating as I have never tried to implement it with Symfony before except when making Prestashop modules. I have omitted some code for brevity but the whole project is available on GITHUB
Prerequisites:
- Some knowledge of PHP, Symfony, Doctrine and JQuery
- Symfony 4.4
- Jquery sortable plugin
- Gedmo doctrine-extensions
1. Add the required Javascript and CSS dependencies
We will need Bootstrap , JQuery, and the JQuery orderable plugin. The easiest way is including them into the base.html.twig file in their corresponding sections (css, or script)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Pizza's Application{% endblock %}</title>
{% block stylesheets %}
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/fontawesome.min.css">
{% endblock %}
{% block javascripts %}
<script src="https://code.jquery.com/jquery-3.2.1.min.js"
integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js"
integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh"
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js"
integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ"
crossorigin="anonymous">
</script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
2. Adding Gedmo extension to the project
composer require gedmo/doctrine-extensions
3. Add the new mapping to Doctrine configuration file. See the original file HERE
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
orm:
auto_generate_proxy_classes: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
auto_mapping: true
mappings:
/* More code omitted for brevity */
Sortable:
type: annotation
alias: Gedmo
prefix: Gedmo\Sortable\Entity
dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/src/Sortable/Entity"
4. Add the new Gedmo listener. See the original file HERE
/* More code omitted for brevity */
gedmo.listener.sortable:
class: Gedmo\Sortable\SortableListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ "@annotation_reader" ] ]
5. Make our entity orderable. See the original file HERE
In our PizzaIngredient entity we have two properties to configure. The first is the order that our ingredients will have in the pizza adding the annotation @Gedmo\SortablePosition and the next step will be to configure (@Gedmo\SortableGroup) the entity property that will be used for grouping those ingredients. This part is important because into the repository we will have all the pizza and their ingredients. So telling Gedmo to group by this field will make him aware of that the numbering (first & end ) is restricted to every group of pizza
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Gedmo\Mapping\Annotation\Sortable;
/**
* PizzaIngredient
* @ORM\Entity
* @ORM\Table(name="pizzaIngredient")
* @ORM\Entity(repositoryClass="App\Repository\PizzaIngredientRepository")
*/
class PizzaIngredient
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @Gedmo\SortablePosition
* @ORM\Column(name="ingredientOrder",type="integer")
*/
private $ingredientOrder;
/**
* @var Pizza
* @Gedmo\SortableGroup
* @ORM\ManyToOne(targetEntity="Pizza")
*/
private $pizza;
/* More code omitted for brevity */
6. Prepare our twig template. See the original file HERE
Here I just show the section related with the creation of the list of ingredients. It is just a simple twig for-loop. The key here is to set the id attribute to the parent container (ul id=”sortable”)which will be used later by the JQuery UI plugin to detect the drag and drop event and do the Ajax request to the server to update the new order of the ingredient. Yes, there is some ugly inline code there 🙁 . Do a pull request to fix it 🙂
{% extends 'base.html.twig' %}
{% block body %}
/* More code omitted for brevity */
<h3>EDIT THE PIZZA</h3>
{{ form_start(edit_form) }}
{{ form_end(edit_form) }}
<div style="margin: 20px; ">
<div style="margin: auto; display: inline-block;padding: 20px">
<ul id="sortable" style="text-align: left ;list-style: none">
{% for pizzaIngredient in pizza.pizzaIngredients %}
<li id="{{ pizzaIngredient.id }}" draggable="true" class="ui-state-default"
rel="{{ pizzaIngredient.id }} "
style="margin: 10px;background-color: #6897BB;font-weight:500;color:white;padding: 5px">
<a href="{{ path('pizza_ingredient_delete', { 'id': pizzaIngredient.id }) }}"><img
style="max-width: 20px;" alt="delete" src="{{ '/public/img/deleteicon.svg' }}"></a>
{{ pizzaIngredient.ingredient.name }}
{{ pizzaIngredient.ingredientOrder }}
</li>
</tr>{% endfor %}
</ul>
</div>
/* More code omitted for brevity */
{% endblock %}
7. Add the JQuery function to update the new position. See the original file HERE
I decided to put the JavaScript code at the end of the edit.html.twig file for simplicity but the right place should be in the JavaScript block of the base template (base.html.twig). I have forced the value of the link variable but you may configure it dynamically using the twig path generation function ej. {{ path(‘my path name goes here’) }}
<script>
$('#sortable').sortable(
{
stop: function (event, ui) {
let position = ui.item.index();
let link = '/public/update-ingredient-position';
let element_id = ui.item.attr('id');
//let pizzaId = $('app_pizza_id').val();
$.ajax({
type: "POST",
url: link,
data: {
'position': position,
'pizzaIngredientId': element_id
},
success: function (result) {
console.log(result);
},
error: function (error) {
}
});
}
});
</script>
8. Create Controller action to update position. See the original file HERE
Here the controller receives by POST the data sent by the ajax, process it and return a response that may be used to show a success message to the user or if you want to go further set back the ingredient to its previous state
/**
* Pizza controller.
* @Route("/")
*/
class PizzaController extends AbstractController
{
/* More code omitted for brevity */
/**
*
* @Route("/update-ingredient-position", name="pizza_ingredient_update_position")
* @param Request $request
* @param PizzaService $pizzaService
*
* @return Jsonresponse
*/
public function updateIngredientPositionAction(Request $request, PizzaService $pizzaService): Jsonresponse
{
$ingredientId = $request->get('pizzaIngredientId');
$position = $request->get('position');
try {
$pizzaService->updatePizzaIngredientPosition($ingredientId, $position);
return new jsonresponse(true);
} catch (Exception $e) {
return new jsonresponse(false);
}
}
A final note
Don’t forget to add the following annotation (@OrderBy({“ingredientOrder” = “ASC”})) to the Pizza Entity. When the Pizza entity was defined, an attribute named pizzaIngredients was set. This attribute is just a Collection of PizzaIngredient entities. The data in the collection is retrieved by Doctrine by default unordered, therefore if you want to print the list of ingredients in correct order don’t miss this part
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\OrderBy;
/**
* Pizza
* @ORM\Entity
* @ORM\Table(name="pizza")
* @ORM\Entity(repositoryClass="App\Repository\PizzaRepository")
*/
class Pizza
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=180)
*/
private $name;
/**
* @var Collection|PizzaIngredient[]
* @ORM\OneToMany(targetEntity="PizzaIngredient", mappedBy="pizza", cascade={"persist", "remove"})
* @OrderBy({"ingredientOrder" = "ASC"})
*/
private $pizzaIngredients;
private $price;
/* More code omitted for brevity */








