Create Basic Grid

In this cookbook, we will create a new grid to display salesmen data in the administration. We will learn how to use the grid factory properly, configure the data displayed, and override the default data presentation. In the end, we will be able to even sort our grid by column, paginate large result sets and delete records.

Prerequisites

  • As we are going to list the salesmen, you need to create the Salesman entity and add some entries to the database table following the cookbook, first.
  • You should know how to work with controllers, its actions and be familiar with concepts of administration pages in Shopsys Platform.

1. Define the grid

1.1 Create grid factory

First, we need to create a factory responsible for the creation of our new grid.

In App\Grid\Salesman namespace we create the new class that will implement GridFactoryInterface. This interface forces us to implement SalesmanGridFactory::create method responsible for creating the grid itself.

General class GridFactory helps us prepare the grid so that we can request this service in the constructor.

declare(strict_types=1);

namespace App\Grid\Salesman;

use Shopsys\FrameworkBundle\Component\Grid\Grid;
use Shopsys\FrameworkBundle\Component\Grid\GridFactory;
use Shopsys\FrameworkBundle\Component\Grid\GridFactoryInterface;

class SalesmanGridFactory implements GridFactoryInterface
{
    /**
     * @var \Shopsys\FrameworkBundle\Component\Grid\GridFactory
     */
    protected $gridFactory;

    /**
     * @param \Shopsys\FrameworkBundle\Component\Grid\GridFactory $gridFactory
     */
    public function __construct(GridFactory $gridFactory)
    {
        $this->gridFactory = $gridFactory;
    }

    /**
     * @return \Shopsys\FrameworkBundle\Component\Grid\Grid
     */
    public function create(): Grid
    {
        /* @TODO: implement */
    }
}

In services.yaml, we need to register this new class.

App\Grid\Salesman\SalesmanGridFactory: ~

1.2 Configure grid data source

GridFactory::create() requires an implementation of DataSourceInterface as its second argument. So we create one that returns all the salesmen we need.

In our SalesmanGridFactory, we add a new protected method, that creates and returns data source. And because we want to get data from the database, we use a data source created from Doctrine Query Builder.

+ use Doctrine\ORM\EntityManagerInterface;

+   /**
+    * @var \Doctrine\ORM\EntityManagerInterface
+    */
+   protected $entityManager;

-   public function __construct(GridFactory $gridFactory)
+   public function __construct(GridFactory $gridFactory, EntityManagerInterface $entityManager)
    {
        $this->gridFactory = $gridFactory;
+       $this->entityManager = $entityManager;
    }

    /**
     * @return \Shopsys\FrameworkBundle\Component\Grid\Grid
     */
    public function create(): Grid
    {
-       /* @TODO: implement */
+       $grid = $this->gridFactory->create('salesmanGrid', $this->createAndGetDataSource());

+       return $grid;
    }

Now, let's implement createAndGetDataSource method that should be in the same SalesmanGridFactory and will look like this.

use App\Model\Salesman\Salesman;
use Shopsys\FrameworkBundle\Component\Grid\DataSourceInterface;
use Shopsys\FrameworkBundle\Component\Grid\QueryBuilderDataSource;

// ...

    /**
     * @return \Shopsys\FrameworkBundle\Component\Grid\DataSourceInterface
     */
    protected function createAndGetDataSource(): DataSourceInterface
    {
        $queryBuilder = $this->entityManager->createQueryBuilder();

        $queryBuilder->select('s')
            ->from(Salesman::class, 's');

        return new QueryBuilderDataSource($queryBuilder, 's.id');
    }

Do not forget to add import of App\Model\Salesman\Salesman to SalesmanGridFactory.

1.3 Add columns to the grid

We prepared our grid, but for now, it is not rendered anywhere and does not contain any columns. We are going to change this now.

First, we add columns we want to see into SalesmanGridFactory::create method.

    public function create(): Grid
    {
        $grid = $this->gridFactory->create('salesmanGrid', $this->createAndGetDataSource());

+       $grid->addColumn('id', 's.id', t('Id'));
+       $grid->addColumn('name', 's.name', t('Name'));
+       $grid->addColumn('registeredAt', 's.registeredAt', t('Registered'));

        return $grid;
    }

Note

In the example above, the column names are translated. Do not forget to dump translations.

2. Display the grid

Grid is ready to show all the salesmen from the database. And we just need to render the grid itself using a controller.

2.1 Create a new controller & action

We must inject (pass through the constructor) SalesmanGridFactory created earlier and pass the grid view to the template.

namespace App\Controller\Admin;

use App\Grid\Salesman\SalesmanGridFactory;
use Shopsys\FrameworkBundle\Controller\Admin\AdminBaseController;
use Symfony\Component\Routing\Annotation\Route;

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

    /**
     * @param \App\Grid\Salesman\SalesmanGridFactory $salesmanGridFactory
     */
    public function __construct(SalesmanGridFactory $salesmanGridFactory)
    {
        $this->salesmanGridFactory = $salesmanGridFactory;
    }

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

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

2.2 Create a new template

Finally, it is time to create a new twig template, templates/Admin/Content/Salesman/list.html.twig with the following content:

{% extends '@ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %}

{% block title %}- {{ 'Salesmen'|trans }}{% endblock %}
{% block h1 %}{{ 'Salesmen'|trans }}{% endblock %}

{% block main_content %}
    {{ gridView.render() }}
{% endblock %}

Now, you should be able to see the basic grid with salesmen data when accessing /admin/salesman/list/ Basic Grid

Note

If you want to add a link to the page to the menu and proper breadcrumb navigation, please check the corresponding section in Adding a New Administration Page cookbook.

3. Modify the basic grid appearance

As you probably noticed, dates in the third column are not printed much friendly. To adjust appearance (e.g., let's say we are in Germany and want to format the dates appropriately), we just need to extend the default grid template and modify it accordingly.

3.1 Create a new template

We create the new twig template listGrid.html.twig in templates/Admin/Content/Salesman. The template has to extend @ShopsysFramework/Admin/Grid/Grid.html.twig and override block grid_value_cell_id_registeredAt where we apply a Twig filter to the value. You can read more about blocks here.

{% extends '@ShopsysFramework/Admin/Grid/Grid.html.twig' %}

{% block grid_value_cell_id_registeredAt %}
    {{ value|formatDateTime('de') }}
{% endblock %}

3.2 Set the grid theme

Now that we have template ready, we just need to set it as the theme in the grid factory.

// src/Grid/Salesman/SalesmanGridFactory

    public function create(): Grid
    {
        $grid = $this->gridFactory->create('salesmanGrid', $this->createAndGetDataSource());

        ...

+       $grid->setTheme('Admin/Content/Salesman/listGrid.html.twig');

        return $grid;
    }

4. Sort rows

Now, we see data as we want, but it would be nice to be able to adjust the view from the user's perspective. We may want to ease the finding of a certain salesman by allowing to sort rows by name or date of creation.

A default order will be by date with the newest salesmen at the top.

// src/Grid/Salesman/SalesmanGridFactory

    public function create(): Grid
    {
        $grid = $this->gridFactory->create('salesmanGrid', $this->createAndGetDataSource());

        $grid->addColumn('id', 's.id', t('Id'));
-       $grid->addColumn('name', 's.name', t('Title'));
+       $grid->addColumn('name', 's.name', t('Title'), true);
-       $grid->addColumn('registeredAt', 's.registeredAt', t('Registered'));
+       $grid->addColumn('registeredAt', 's.registeredAt', t('Registered'), true);

+       $grid->setDefaultOrder('registeredAt', DataSourceInterface::ORDER_DESC);

        ...
   }

5. Paginate results

As the number of salesmen grows, the clarity decreases rapidly. To keep work with grid enjoyable, we can split data across several pages with pagination.

In the grid, we just need to call one method.

// src/Grid/Salesman/SalesmanGridFactory

    public function create(): Grid
    {
        $grid = $this->gridFactory->create('salesmanGrid', $this->createAndGetDataSource());
        ...

+       $grid->enablePaging();

        ...
     }

6. Delete salesmen using the grid

As the last, an admin may want to delete some salesmen. Grid eases the task with an already implemented action column.

6.1 Implement the deletion logic

First, we need to get the salesman entity by its ID and remove it from persistence using the Doctrine entity manager. We will follow the basic concepts of Shopsys Platform (see "Basics about model architecture" article) and create new classes - SalesmanFacade and SalesmanRepository.

6.1.1 Create SalesmanRepository and implement getById method

// src\Model\Salesman\SalesmanRepository.php
declare(strict_types=1);

namespace App\Model\Salesman;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;

class SalesmanRepository
{
    /**
     * @var \Doctrine\ORM\EntityManagerInterface
     */
    protected $em;

    /**
     * @param \Doctrine\ORM\EntityManagerInterface $em
     */
    public function __construct(EntityManagerInterface $em)
    {
        $this->em = $em;
    }

    /**
     * @param int $salesmanId
     * @return \App\Model\Salesman\Salesman
     */
    public function getById(int $salesmanId): Salesman
    {
        $salesman = $this->getSalesmanRepository()->find($salesmanId);

        if ($salesman === null) {
            // you should throw new custom exception here...
        }

        return $salesman;
    }

    /**
     * @return \Doctrine\ORM\EntityRepository
     */
    protected function getSalesmanRepository(): EntityRepository
    {
        return $this->em->getRepository(Salesman::class);
    }
}

6.1.2 Create SalesmanFacade and implement deleteById method

// src\Model\Salesman\SalesmanFacade.php
declare(strict_types=1);

namespace App\Model\Salesman;

use Doctrine\ORM\EntityManagerInterface;

class SalesmanFacade
{
    /**
     * @var \Doctrine\ORM\EntityManagerInterface
     */
    protected $entityManager;

    /**
     * @var \App\Model\Salesman\SalesmanRepository
     */
    protected $salesmanRepository;

    /**
     * @param \Doctrine\ORM\EntityManagerInterface $entityManager
     * @param \App\Model\Salesman\SalesmanRepository $salesmanRepository
     */
    public function __construct(EntityManagerInterface $entityManager, SalesmanRepository $salesmanRepository)
    {
        $this->entityManager = $entityManager;
        $this->salesmanRepository = $salesmanRepository;
    }

    /**
     * @param int $salesmanId
     */
    public function deleteById(int $salesmanId)
    {
        $salesman = $this->salesmanRepository->getById($salesmanId);

        $this->entityManager->remove($salesman);
        $this->entityManager->flush();
    }
}

6.2 Add delete action to the controller

In our SalesmanController, we add the new action to delete the salesman using SalesmanFacade.

// src/Controller/Admin/SalesmanController
namespace App\Controller\Admin;

use App\Grid\Salesman\SalesmanGridFactory;
+ 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\Model\Salesman\SalesmanFacade
+     */
+    protected $salesmanFacade;

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

+    /**
+     * @Route("/salesman/delete/{id}", requirements={"id" = "\d+"})
+     * @CsrfProtection
+     */
+    public function deleteAction($id)
+    {
+        $this->salesmanFacade->deleteById($id);
+
+        $this->getFlashMessageSender()
+            ->addInfoFlash(t('Salesman with id %id% deleted', ['%id%' => $id]));
+
+        return $this->redirectToRoute('admin_salesman_list');
+    }

Tip

It is a good practice to enable CSRF protection on this type of action.

6.3 Add action column to the grid

We just use addDeleteActionColumn in the existing SalesmanGridFactory with arguments admin_salesman_delete as a route and request parameters (the action needs to know the salesman's ID to delete). To prevent accidental deletion, we can also set a confirmation message.

// src/Grid/Salesman/SalesmanGridFactory

    public function create(): Grid
    {
        $grid = $this->gridFactory->create('salesmanGrid', $this->createAndGetDataSource());

        $grid->addColumn('id', 's.id', t('Id'));
        $grid->addColumn('name', 's.name', t('Title'));
        $grid->addColumn('registeredAt', 's.registeredAt', t('Registered'));

+       $grid->addDeleteActionColumn('admin_salesman_delete', ['id' => 's.id'])
+           ->setConfirmMessage(t('Do you really want to remove this salesman?'));
    ...

Conclusion

Now, you should be able to create a new grid and use the strengths of the grid component to display, sort and delete salesmen. A similar approach could be used to create a grid that precisely presents any data in a way you need.

Basic grid with pagination, ordering and delete column