You are browsing the latest version of the documentation which is not released yet. See the current stable version instead.

Create Basic Grid

In this cookbook, we will create a new grid to display salesmen data in the administration. We will learn how to properly use grid factory, how to configure 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 Framework.

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 with preparation of the grid, so we will 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 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 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 grid itself using a controller.

2.1 Create a new controller & action

We need to inject (pass through constructor) SalesmanGridFactory created earlier and pass 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 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 users perspective. We may want to ease finding of a certain salesman by allowing to sort rows by name or date of creation.

A default order will be by date with 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 grow, the clarity decreases rapidly. To keep work with grid enjoyable, we can split data across several pages with pagination.

In the grid is just a matter of calling 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 already implemented action column.

6.1 Implement the deletion logic

First, we need to get the salesman entity by its id and then remove it from persistence using Doctrine entity manager. We will follow the basic concepts of Shopsys Framework (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 actions.

6.3 Add action column to the grid

We just use addDeleteActionColumn in existing SalesmanGridFactory with arguments admin_salesman_delete as a route and request parameters (action have to know id of the salesman 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 to 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