Create Advanced Grid

This article provides step by step instructions for advanced grid configurations. After finishing this cookbook, you will know how to create a grid with inline editing, and drag&drop sorting.

Prerequisites

  • you have created a grid for Salesman entity following Create Basic Grid cookbook
  • you are aware of Shopsys Framework model concepts like entity data classes and their factories, facades etc.
  • a basic knowledge of Symfony forms might be helpful for you

1. Allow inline editing

In this step, we will allow creating and editing of salesmen entities (that we worked with in the previous cookbook) directly using the grid. As a preparation for that, we need to implement the creation and editing logic, first.

1.1 Create SalesmanData class

// src/Model/Salesman/SalesmanData.php

declare(strict_types=1);

namespace App\Model\Salesman;

class SalesmanData
{
    /**
     * @var string|null
     */
    public $name;

    /**
     * @var \DateTime|null
     *
     */
    public $registeredAt;
}

1.2 Add constructor, edit and setData method, and getters to Salesman entity

// src/Model/Salesman/Salesman.php

class Salesman
{
+     /**
+      * @param \App\Model\Salesman\SalesmanData $salesmanData
+      */
+     public function __construct(SalesmanData $salesmanData)
+     {
+         $this->setData($salesmanData);
+     }

+     /**
+      * @param \App\Model\Salesman\SalesmanData $salesmanData
+      */
+     public function edit(SalesmanData $salesmanData)
+     {
+         $this->setData($salesmanData);
+     }
+
+      * @param \App\Model\Salesman\SalesmanData $salesmanData
+      */
+     public function setData(SalesmanData $salesmanData)
+     {
+         $this->name = $salesmanData->name;
+         $this->registeredAt = $salesmanData->registeredAt;
+     }

+    /**
+     * @return int
+     */
+    public function getId(): int
+    {
+        return $this->id;
+    }

+    /**
+     * @return string
+     */
+    public function getName(): string
+    {
+        return $this->name;
+    }

+    /**
+     * @return \DateTime
+     */
+    public function getRegisteredAt(): \DateTime
+    {
+        return $this->registeredAt;
+    }
}

1.3 Create SalesmanDataFactory class with create and createFromSalesman methods

// src/Model/Salesman/SalesmanDataFactory.php

declare(strict_types=1);

namespace App\Model\Salesman;

class SalesmanDataFactory
{
    /**
     * @return \App\Model\Salesman\SalesmanData
     */
    public function create(): SalesmanData
    {
        $salesmanData = new SalesmanData();
        $salesmanData->registeredAt = new \DateTime();

        return $salesmanData;
    }

    /**
     * @param \App\Model\Salesman\Salesman $salesman
     * @return \App\Model\Salesman\SalesmanData
     */
    public function createFromSalesman(Salesman $salesman): SalesmanData
    {
        $salesmanData = new SalesmanData();
        $salesmanData->name = $salesman->getName();
        $salesmanData->registeredAt = $salesman->getRegisteredAt();

        return $salesmanData;
    }
}

1.4 Add create, edit, and getById methods into SalesmanFacade class

// src/Model/Salesman/SalesmanFacade.php

class SalesmanFacade
{
+    /**
+     * @param \App\Model\Salesman\SalesmanData $salesmanData
+     * @return \App\Model\Salesman\Salesman
+     */
+    public function create(SalesmanData $salesmanData): Salesman
+    {
+        $salesman = new Salesman($salesmanData);
+        $this->entityManager->persist($salesman);
+        $this->entityManager->flush();
+
+        return $salesman;
+    }
+
+    /**
+     * @param int $salesmanId
+     * @param \App\Model\Salesman\SalesmanData $salesmanData
+     * @return \App\Model\Salesman\Salesman
+     */
+    public function edit(int $salesmanId, SalesmanData $salesmanData): Salesman
+    {
+        $salesman = $this->getById($salesmanId);
+        $salesman->edit($salesmanData);
+        $this->entityManager->flush();
+
+        return $salesman;
+    }

+    /**
+     * @param $salesmanId
+     * @return \App\Model\Salesman\Salesman
+     */
+    public function getById($salesmanId): Salesman
+    {
+        return $this->salesmanRepository->getById($salesmanId);
+    }
}

1.5 Create new form defined by SalesmanFormType class

When using a grid for inline editing, a form is rendered in the grid row. We need to prepare that form now.

// src/Form/Admin/SalesmanFormType.php

declare(strict_types=1);

namespace App\Form\Admin;

use App\Model\Salesman\SalesmanData;
use Shopsys\FrameworkBundle\Form\DatePickerType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints;

class SalesmanFormType extends AbstractType
{
    /**
     * @param \Symfony\Component\Form\FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', TextType::class, [
                'constraints' => [
                    new Constraints\NotBlank(['message' => 'Please enter name']),
                ],
            ])
            ->add('registeredAt', DatePickerType::class, [
                'constraints' => [
                    new Constraints\NotBlank(['message' => 'Please enter date of registration']),
                ],
            ]);
    }

    /**
     * @param \Symfony\Component\OptionsResolver\OptionsResolver $resolver
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => SalesmanData::class,
            'attr' => ['novalidate' => 'novalidate'],
        ]);
    }
}

1.6 Create new SalesmanGridInlineEdit class

Now, we have everything prepared and we are able to put it all together in new class (SalesmanGridInlineEdit) that will be responsible for the inline editing. The class needs to extend AbstractGridInlineEdit and implement three methods -getForm, editEntity, and createEntityAndGetId. We also have to inject the original SalesmanGridFactory to the new class constructor.

// src/Grid/Salesman/SalesmanGridInlineEdit.php

namespace App\Grid\Salesman;

use App\Form\Admin\SalesmanFormType;
use App\Model\Salesman\SalesmanDataFactory;
use App\Model\Salesman\SalesmanFacade;
use Shopsys\FrameworkBundle\Component\Grid\InlineEdit\AbstractGridInlineEdit;
use Symfony\Component\Form\FormFactoryInterface;

class SalesmanGridInlineEdit extends AbstractGridInlineEdit
{
    /**
     * @var \App\Grid\Salesman\SalesmanGridFactory
     */
    private $salesmanGridFactory;

    /**
     * @var \App\Model\Salesman\SalesmanFacade
     */
    private $salesmanFacade;

    /**
     * @var \Symfony\Component\Form\FormFactoryInterface
     */
    private $formFactory;

    /**
     * @var \App\Model\Salesman\SalesmanDataFactory
     */
    private $salesmanDataFactory;

    public function __construct(
        SalesmanGridFactory $salesmanGridFactory,
        SalesmanFacade $salesmanFacade,
        FormFactoryInterface $formFactory,
        SalesmanDataFactory $salesmanDataFactory
    ) {
        parent::__construct($salesmanGridFactory);
        $this->salesmanGridFactory = $salesmanGridFactory;
        $this->salesmanFacade = $salesmanFacade;
        $this->formFactory = $formFactory;
        $this->salesmanDataFactory = $salesmanDataFactory;
    }

    /**
     * @param int|null $salesmanId
     * @return \Symfony\Component\Form\FormInterface
     */
    public function getForm($salesmanId)
    {
        if ($salesmanId === null) {
            $salesmanData = $this->salesmanDataFactory->create();
        } else {
            $salesman = $this->salesmanFacade->getById($salesmanId);
            $salesmanData = $this->salesmanDataFactory->createFromSalesman($salesman);
        }

        return $this->formFactory->create(SalesmanFormType::class, $salesmanData);
    }

    /**
     * @param int $salesmanId
     * @param \App\Model\Salesman\SalesmanData $salesmanData
     */
    protected function editEntity($salesmanId, $salesmanData)
    {
        $this->salesmanFacade->edit($salesmanId, $salesmanData);
    }

    /**
     * @param \App\Model\Salesman\SalesmanData $salesmanData
     * @return int
     */
    protected function createEntityAndGetId($salesmanData)
    {
        $salesman = $this->salesmanFacade->create($salesmanData);

        return $salesman->getId();
    }
}

The new class must be registered in services.yaml:

    App\Grid\Salesman\SalesmanGridInlineEdit: ~

1.7 Use SalesmanGridInlineEdit in SalesmanController

To make the salesman grid inline editable now, we need to use the SalesmanGridInlineEdit::getGrid method to get the grid instead of calling SalesmanGridFactory::create method directly:

// src/Controller/Admin/SalesmanController.php

namespace App\Controller\Admin;

-use App\Grid\Salesman\SalesmanGridFactory;
+use App\Grid\Salesman\SalesmanGridInlineEdit;
use App\Model\Salesman\SalesmanFacade;
use Shopsys\FrameworkBundle\Component\Router\Security\Annotation\CsrfProtection;
use Shopsys\FrameworkBundle\Controller\Admin\AdminBaseController;
use Symfony\Component\Routing\Annotation\Route;

class SalesmanController extends AdminBaseController
{
    /**
-     * @var \App\Grid\Salesman\SalesmanGridFactory
+     * @var \App\Grid\Salesman\SalesmanGridInlineEdit
     */
-    protected $salesmanGridFactory;
+    protected $salesmanGridInlineEdit;

    /**
     * @var \App\Model\Salesman\SalesmanFacade
     */
    protected $salesmanFacade;

-    public function __construct(SalesmanGridFactory $salesmanGridFactory, SalesmanFacade $salesmanFacade)
+    public function __construct(SalesmanGridInlineEdit $salesmanGridInlineEdit, SalesmanFacade $salesmanFacade)
    {
-        $this->salesmanGridFactory = $salesmanGridFactory;
+        $this->salesmanGridInlineEdit = $salesmanGridInlineEdit;
        $this->salesmanFacade = $salesmanFacade;
    }

    /**
     * @Route("/salesman/list/")
     */
    public function listAction()
    {
-        $grid = $this->salesmanGridFactory->create();
+        $grid = $this->salesmanGridInlineEdit->getGrid();

        return $this->render('Admin/Content/Salesman/list.html.twig', [
            'gridView' => $grid->createView(),
        ]);
    }
}

At this point, you should be able to edit and create new salesmen directly in the grid.

Advanced grid with inline edit

2. Sort data manually (drag and drop)

In this part, we will enable drag and drop sorting of our salesmen using the grid. To make the changes in the ordering persistent, we need add new attribute to Salesman entity, first.

2.1 Add $position to Salesman entity and mark it as DB column using Doctrine ORM annotation

// src/Model/Salesman/Salesman.php

class Salesman
{
+    /**
+     * @var int|null
+     *
+     * @ORM\Column(type="integer", nullable=true)
+     */
+    protected $position;
}

2.2 Generate new database migration

Run phing target

php phing db-migrations-generate

The command prints a file name the migration was generated into. The migration will look like this:

namespace App\Migrations;

use Doctrine\DBAL\Schema\Schema;
use Shopsys\MigrationBundle\Component\Doctrine\Migrations\AbstractMigration;

class Version20190305140005 extends AbstractMigration
{
    /**
     * @param \Doctrine\DBAL\Schema\Schema $schema
     */
    public function up(Schema $schema): void
    {
        $this->sql('ALTER TABLE salesmen ADD position INT DEFAULT NULL');
    }

    /**
     * @param \Doctrine\DBAL\Schema\Schema $schema
     */
    public function down(Schema $schema): void
    {
    }
}

2.3 Execute migrations to propagate all the changes to the database

Run phing target

php phing db-migrations

2.2 Make the Salesman entity implement OrderableEntityInterface

// src/Model/Salesman/Salesman.php

+ use Shopsys\FrameworkBundle\Component\Grid\Ordering\OrderableEntityInterface;

- class Salesman
+ class Salesman implements OrderableEntityInterface
{
+    /**
+     * @param int $position
+     */
+    public function setPosition($position)
+    {
+        $this->position = $position;
+    }
}

2.3 Enable drag and drop sorting in SalesmanGridFactory

// src/Grid/Salesman/SalesmanGridFactory.php

class SalesmanGridFactory implements GridFactoryInterface
{
    public function create(): Grid
    {
        ...
+       $grid->enableDragAndDrop(Salesman::class);
        ...
    {
}

Now you should be able to sort your salesmen using the cross icon in the left part of each row as a handle for drag a drop.

Advanced grid with drag and drop

Pitfalls

Be aware of using all the combinations that grid provides, e.g. it is not possible to use sorting by column when drag and drop is enabled.