<?php
declare(strict_types=1);
namespace App\CoreBundle\Component\Grid;
use App\CoreBundle\Component\Grid\InlineEdit\GridInlineEditInterface;
use App\CoreBundle\Component\Grid\ModalEdit\GridModalEditInterface;
use App\CoreBundle\Component\Router\Security\RouteCsrfProtector;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\RouterInterface;
use Twig\Environment as Twig_Environment;
class Grid
{
public const GET_PARAMETER = 'g';
public const DEFAULT_VIEW_THEME = 'Admin/Grid/Grid.html.twig';
public const DEFAULT_LIMIT = 10;
/**
* @var string
*/
protected $id;
/**
* @var \App\CoreBundle\Component\Grid\Column[]
*/
protected $columnsById = [];
/**
* @var \App\CoreBundle\Component\Grid\ActionColumn[]
*/
protected $actionColumns = [];
/**
* @var \App\CoreBundle\Component\Grid\GroupActionColumn[]
*/
protected $groupActionColumns = [];
/**
* @var bool
*/
protected $enablePaging = false;
/**
* @var bool
*/
protected $enableSelecting = false;
/**
* @var array
*/
protected $allowedLimits = [10, 30, 100, 200, 500, 1000, 2500, 5000, 10000];
/**
* @var int
*/
protected $limit;
/**
* @var bool
*/
protected $isLimitFromRequest = false;
/**
* @var int
*/
protected $page = 1;
/**
* @var int|null
*/
protected $totalCount;
/**
* @var int|null
*/
protected $pageCount;
/**
* @var string|null
*/
protected $orderSourceColumnName;
/**
* @var string|null
*/
protected $orderDirection;
/**
* @var bool
*/
protected $isOrderFromRequest = false;
/**
* @var array
*/
protected $rows = [];
/**
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* @var \Symfony\Component\Routing\RouterInterface
*/
protected $router;
/**
* @var \App\CoreBundle\Component\Router\Security\RouteCsrfProtector
*/
protected $routeCsrfProtector;
/**
* @var \Twig\Environment
*/
protected $twig;
/**
* @var \App\CoreBundle\Component\Grid\DataSourceInterface
*/
protected $dataSource;
/**
* @var string
*/
protected $actionColumnClassAttribute = '';
/**
* @var \App\CoreBundle\Component\Grid\InlineEdit\GridInlineEditInterface|null
*/
protected $inlineEditService;
/**
* @var \App\CoreBundle\Component\Grid\ModalEdit\GridModalEditInterface|null
*/
protected $modalEditService;
/**
* @var string|null
*/
protected $orderingEntityClass;
/**
* @var \App\CoreBundle\Component\Paginator\PaginationResult
*/
protected $paginationResults;
/**
* @var string|string[]|null
*/
protected $viewTheme;
/**
* @var array
*/
protected $viewTemplateParameters;
/**
* @var array
*/
protected $selectedRowIds;
/**
* @var bool
*/
protected $multipleDragAndDrop;
/**
* @param string $id
* @param \App\CoreBundle\Component\Grid\DataSourceInterface $dataSource
* @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
* @param \Symfony\Component\Routing\RouterInterface $router
* @param \App\CoreBundle\Component\Router\Security\RouteCsrfProtector $routeCsrfProtector
* @param \Twig\Environment $twig
*/
public function __construct(
$id,
DataSourceInterface $dataSource,
RequestStack $requestStack,
RouterInterface $router,
RouteCsrfProtector $routeCsrfProtector,
Twig_Environment $twig
) {
if (empty($id)) {
$message = 'Grid id cannot be empty.';
throw new \App\CoreBundle\Component\Grid\Exception\EmptyGridIdException($message);
}
$this->id = $id;
$this->dataSource = $dataSource;
$this->requestStack = $requestStack;
$this->router = $router;
$this->routeCsrfProtector = $routeCsrfProtector;
$this->twig = $twig;
$this->limit = static::DEFAULT_LIMIT;
$this->page = 1;
$this->viewTheme = static::DEFAULT_VIEW_THEME;
$this->viewTemplateParameters = [];
$this->selectedRowIds = [];
$this->multipleDragAndDrop = false;
$this->loadFromRequest();
}
/**
* @param string $id
* @param string $sourceColumnName
* @param string $title
* @param bool $sortable
* @return \App\CoreBundle\Component\Grid\Column
*/
public function addColumn($id, $sourceColumnName, $title, $sortable = false, $modalEdit = false)
{
if (array_key_exists($id, $this->columnsById)) {
throw new \App\CoreBundle\Component\Grid\Exception\DuplicateColumnIdException(
'Duplicate column id "' . $id . '" in grid "' . $this->id . '"'
);
}
$column = new Column($id, $sourceColumnName, $title, $sortable, $modalEdit);
$this->columnsById[$id] = $column;
return $column;
}
/**
* @param string $type
* @param string $name
* @param string $route
* @param array $bindingRouteParams
* @param array $additionalRouteParams
* @param string|null $group
* @return \App\CoreBundle\Component\Grid\ActionColumn
*/
public function addActionColumn(
$type,
$name,
$route,
array $bindingRouteParams = [],
array $additionalRouteParams = [],
?string $group = null
) {
$actionColumn = new ActionColumn(
$this->router,
$this->routeCsrfProtector,
$type,
$name,
$route,
$bindingRouteParams,
$additionalRouteParams
);
if ($group) {
$this->groupActionColumns[$group]->addActionColumn($actionColumn);
} else {
$this->actionColumns[] = $actionColumn;
}
return $actionColumn;
}
/**
* @param string $name
* @return \App\CoreBundle\Component\Grid\GroupActionColumn
*/
public function addActionGroupColumn($name) {
$groupActionColumn = new GroupActionColumn($name);
$this->groupActionColumns[$name] = new GroupActionColumn($name);
return $groupActionColumn;
}
/**
* @param string $route
* @param array $bindingRouteParams
* @param array $additionalRouteParams
* @param string|null $group
* @return \App\CoreBundle\Component\Grid\ActionColumn
*/
public function addShowActionColumn($route, array $bindingRouteParams = [], array $additionalRouteParams = [], $group = null)
{
return $this->addActionColumn(ActionColumn::TYPE_SHOW, t('Show'), $route, $bindingRouteParams, $additionalRouteParams, $group);
}
/**
* @param string $route
* @param array $bindingRouteParams
* @param array $additionalRouteParams
* @param string|null $group
* @return \App\CoreBundle\Component\Grid\ActionColumn
*/
public function addProductMatchingActionColumn($route, array $bindingRouteParams = [], array $additionalRouteParams = [], $group = null)
{
return $this->addActionColumn(ActionColumn::TYPE_PRODUCT_MATCH, t('Product match'), $route, $bindingRouteParams, $additionalRouteParams, $group);
}
/**
* @param string $route
* @param array $bindingRouteParams
* @param array $additionalRouteParams
* @param string|null $group
* @return \App\CoreBundle\Component\Grid\ActionColumn
*/
public function addEditActionColumn($route, array $bindingRouteParams = [], array $additionalRouteParams = [], $group = null)
{
return $this->addActionColumn(ActionColumn::TYPE_EDIT, t('Edit'), $route, $bindingRouteParams, $additionalRouteParams, $group);
}
/**
* @param string $route
* @param array $bindingRouteParams
* @param array $additionalRouteParams
* @param string|null $group
* @return \App\CoreBundle\Component\Grid\ActionColumn
*/
public function addSetAsPaidActionColumn($route, array $bindingRouteParams = [], array $additionalRouteParams = [], $group = null)
{
return $this->addActionColumn(ActionColumn::TYPE_SET_AS_PAID, t('Set as paid'), $route, $bindingRouteParams, $additionalRouteParams, $group);
}
/**
* @param string $route
* @param array $bindingRouteParams
* @param array $additionalRouteParams
* @param string|null $group
* @return \App\CoreBundle\Component\Grid\ActionColumn
*/
public function addDeleteActionColumn($route, array $bindingRouteParams = [], array $additionalRouteParams = [], $group = null)
{
return $this->addActionColumn(ActionColumn::TYPE_DELETE, t('Delete'), $route, $bindingRouteParams, $additionalRouteParams, $group);
}
/**
* @param string $route
* @param array $bindingRouteParams
* @param array $additionalRouteParams
* @param string $id
* @param string|null $group
* @return \App\CoreBundle\Component\Grid\ActionColumn
*/
public function addShowLogActionColumn($route, array $bindingRouteParams = [], array $additionalRouteParams = [], $id = '', $group = null)
{
return $this->addActionColumn(ActionColumn::TYPE_SHOW, $id, $route, $bindingRouteParams, $additionalRouteParams, $group);
}
/**
* @param string $route
* @param array $bindingRouteParams
* @param array $additionalRouteParams
* @param string $id
* @param string|null $group
* @return \App\CoreBundle\Component\Grid\ActionColumn
*/
public function addActivateActionColumn($route, array $bindingRouteParams = [], array $additionalRouteParams = [], $id = '', $group = null)
{
return $this->addActionColumn(ActionColumn::TYPE_ACTIVATE, $id, $route, $bindingRouteParams, $additionalRouteParams, $group);
}
/**
* @param string $route
* @param array $bindingRouteParams
* @param array $additionalRouteParams
* @param string $id
* @param string|null $group
* @return \App\CoreBundle\Component\Grid\ActionColumn
*/
public function addRenewActionColumn($route, array $bindingRouteParams = [], array $additionalRouteParams = [], $id = '', $group = null)
{
return $this->addActionColumn(ActionColumn::TYPE_RENEW, $id, $route, $bindingRouteParams, $additionalRouteParams, $group);
}
/**
* @param string $route
* @param array $bindingRouteParams
* @param array $additionalRouteParams
* @param string $id
* @param string|null $group
* @return \App\CoreBundle\Component\Grid\ActionColumn
*/
public function addDuplicateActionColumn($route, array $bindingRouteParams = [], array $additionalRouteParams = [], $id = '', $group = null)
{
return $this->addActionColumn(ActionColumn::TYPE_DUPLICATE, $id, $route, $bindingRouteParams, $additionalRouteParams, $group);
}
/**
* @param string $route
* @param array $bindingRouteParams
* @param array $additionalRouteParams
* @param string $id
* @param string|null $group
* @return \App\CoreBundle\Component\Grid\ActionColumn
*/
public function addUpgradeActionColumn($route, array $bindingRouteParams = [], array $additionalRouteParams = [], $id = '', $group = null)
{
return $this->addActionColumn(ActionColumn::TYPE_UPGRADE, $id, $route, $bindingRouteParams, $additionalRouteParams, $group);
}
/**
* @param string $route
* @param array $bindingRouteParams
* @param array $additionalRouteParams
* @param string $id
* @param string|null $group
* @return \App\CoreBundle\Component\Grid\ActionColumn
*/
public function addRunActionColumn($route, array $bindingRouteParams = [], array $additionalRouteParams = [], $id = '', $group = null)
{
return $this->addActionColumn(ActionColumn::TYPE_RUN, $id, $route, $bindingRouteParams, $additionalRouteParams, $group);
}
/**
* @param string $route
* @param array $bindingRouteParams
* @param array $additionalRouteParams
* @param string $id
* @param string|null $group
* @return \App\CoreBundle\Component\Grid\ActionColumn
*/
public function addLoginActionColumn($route, array $bindingRouteParams = [], array $additionalRouteParams = [], $id = '', $group = null)
{
return $this->addActionColumn(ActionColumn::TYPE_LOGIN, $id, $route, $bindingRouteParams, $additionalRouteParams, $group);
}
/**
* @param string $route
* @param array $bindingRouteParams
* @param array $additionalRouteParams
* @param string $id
* @param string|null $group
* @return \App\CoreBundle\Component\Grid\ActionColumn
*/
public function addCancelActionColumn($route, array $bindingRouteParams = [], array $additionalRouteParams = [], $id = '', $group = null)
{
return $this->addActionColumn(ActionColumn::TYPE_CANCEL, $id, $route, $bindingRouteParams, $additionalRouteParams, $group);
}
/**
* @param \App\CoreBundle\Component\Grid\InlineEdit\GridInlineEditInterface $inlineEditService
*/
public function setInlineEditService(GridInlineEditInterface $inlineEditService)
{
$this->inlineEditService = $inlineEditService;
}
/**
* @return bool
*/
public function isInlineEdit()
{
return $this->inlineEditService !== null;
}
/**
* @return \App\CoreBundle\Component\Grid\InlineEdit\GridInlineEditInterface|null
*/
public function getInlineEditService()
{
return $this->inlineEditService;
}
/**
* @param \App\CoreBundle\Component\Grid\ModalEdit\GridModalEditInterface $modalEditService
*/
public function setModalEditService(GridModalEditInterface $modalEditService)
{
$this->modalEditService = $modalEditService;
}
/**
* @return bool
*/
public function isModalEdit()
{
return $this->modalEditService !== null;
}
/**
* @return \App\CoreBundle\Component\Grid\ModalEdit\GridModalEditInterface|null
*/
public function getModalEditService()
{
return $this->modalEditService;
}
/**
* @param array $row
* @return mixed
*/
public function getRowId($row)
{
return self::getValueFromRowBySourceColumnName($row, $this->dataSource->getRowIdSourceColumnName());
}
/**
* @param string $classAttribute
*/
public function setActionColumnClassAttribute($classAttribute)
{
$this->actionColumnClassAttribute = $classAttribute;
}
/**
* @param string|string[] $viewTheme
* @param array $viewParameters
*/
public function setTheme($viewTheme, array $viewParameters = [])
{
$this->viewTheme = $viewTheme;
$this->viewTemplateParameters = $viewParameters;
}
/**
* @return \App\CoreBundle\Component\Grid\GridView
*/
public function createView()
{
$gridView = $this->createViewWithoutRows();
if ($this->isEnabledPaging()) {
$this->executeTotalQuery();
}
$this->loadRows();
return $gridView;
}
/**
* @param int $rowId
* @return \App\CoreBundle\Component\Grid\GridView
*/
public function createViewWithOneRow($rowId)
{
$gridView = $this->createViewWithoutRows();
$this->loadRowsWithOneRow($rowId);
return $gridView;
}
/**
* @return \App\CoreBundle\Component\Grid\GridView
*/
public function createViewWithoutRows()
{
$this->rows = [];
$gridView = new GridView(
$this,
$this->requestStack,
$this->router,
$this->twig,
$this->viewTheme,
$this->viewTemplateParameters
);
return $gridView;
}
public function enablePaging()
{
$this->enablePaging = true;
}
public function enableSelecting()
{
$this->enableSelecting = true;
}
/**
* @param int $limit
*/
public function setDefaultLimit($limit)
{
if (!$this->isLimitFromRequest) {
$this->setLimit((int)$limit);
}
}
/**
* @param string $columnId
* @param string $direction
*/
public function setDefaultOrder($columnId, $direction = DataSourceInterface::ORDER_ASC)
{
if (!$this->isOrderFromRequest) {
$prefix = $direction === DataSourceInterface::ORDER_DESC ? '-' : '';
$this->setOrderingByOrderString($prefix . $columnId);
}
}
/**
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* @return \App\CoreBundle\Component\Grid\Column[]
*/
public function getColumnsById()
{
return $this->columnsById;
}
/**
* @param string $columnId
* @return bool
*/
public function existsColumn($columnId)
{
return array_key_exists($columnId, $this->columnsById);
}
/**
* @return \App\CoreBundle\Component\Grid\ActionColumn[]
*/
public function getActionColumns()
{
return $this->actionColumns;
}
/**
* @return \App\CoreBundle\Component\Grid\GroupActionColumn[]
*/
public function getGroupActionColumns()
{
return $this->groupActionColumns;
}
/**
* @return array
*/
public function getRows()
{
return $this->rows;
}
/**
* @return bool
*/
public function isEnabledPaging()
{
return $this->enablePaging;
}
/**
* @return bool
*/
public function isEnabledSelecting()
{
return $this->enableSelecting;
}
/**
* @param array $row
* @return bool
*/
public function isRowSelected(array $row)
{
$rowId = $this->getRowId($row);
return in_array($rowId, $this->selectedRowIds, true);
}
/**
* @return array
*/
public function getSelectedRowIds()
{
return $this->selectedRowIds;
}
/**
* @return int
*/
public function getLimit()
{
return $this->limit;
}
/**
* @param int $limit
*/
protected function setLimit($limit)
{
if (in_array($limit, $this->allowedLimits, true)) {
$this->limit = $limit;
}
}
/**
* @return array
*/
public function getAllowedLimits()
{
return $this->allowedLimits;
}
/**
* @return int|null
*/
public function getTotalCount()
{
return $this->totalCount;
}
/**
* @return int
*/
public function getPage()
{
return $this->page;
}
/**
* @return int
*/
public function getPageCount()
{
return $this->pageCount;
}
/**
* @return string|null
*/
public function getOrderSourceColumnName()
{
return $this->orderSourceColumnName;
}
/**
* @return string|null
*/
public function getOrderSourceColumnNameWithDirection()
{
$prefix = '';
if ($this->getOrderDirection() === DataSourceInterface::ORDER_DESC) {
$prefix = '-';
}
return $prefix . $this->getOrderSourceColumnName();
}
/**
* @return string|null
*/
public function getOrderDirection()
{
return $this->orderDirection;
}
/**
* @return string
*/
public function getActionColumnClassAttribute()
{
return $this->actionColumnClassAttribute;
}
/**
* @return \App\CoreBundle\Component\Paginator\PaginationResult
*/
public function getPaginationResults()
{
return $this->paginationResults;
}
/**
* @param string $orderString
*/
protected function setOrderingByOrderString($orderString)
{
if (substr((string)$orderString, 0, 1) === '-') {
$this->orderDirection = DataSourceInterface::ORDER_DESC;
} else {
$this->orderDirection = DataSourceInterface::ORDER_ASC;
}
$this->orderSourceColumnName = trim($orderString, '-');
}
protected function loadFromRequest()
{
$queryData = $this->requestStack->getMasterRequest()->query->get(self::GET_PARAMETER, []);
if (array_key_exists($this->id, $queryData)) {
$gridQueryData = $queryData[$this->id];
if (array_key_exists('limit', $gridQueryData)) {
$this->setLimit((int)trim($gridQueryData['limit']));
$this->isLimitFromRequest = true;
}
if (array_key_exists('page', $gridQueryData)) {
$this->page = max((int)trim($gridQueryData['page']), 1);
}
if (array_key_exists('order', $gridQueryData)) {
$this->setOrderingByOrderString(trim($gridQueryData['order']));
$this->isOrderFromRequest = true;
}
}
$requestData = $this->requestStack->getMasterRequest()->request->get(self::GET_PARAMETER, []);
if (array_key_exists($this->id, $requestData)) {
$gridRequestData = $requestData[$this->id];
if (array_key_exists('selectedRowIds', $gridRequestData) && is_array($gridRequestData['selectedRowIds'])) {
$this->selectedRowIds = array_map('json_decode', $gridRequestData['selectedRowIds']);
}
}
}
/**
* @param array|string $removeParameters
* @return array
*/
public function getGridParameters($removeParameters = [])
{
$gridParameters = [];
if ($this->isEnabledPaging()) {
$gridParameters['limit'] = $this->getLimit();
if ($this->getPage() > 1) {
$gridParameters['page'] = $this->getPage();
}
}
if ($this->getOrderSourceColumnName() !== null) {
$gridParameters['order'] = $this->getOrderSourceColumnNameWithDirection();
}
foreach ((array)$removeParameters as $parameterToRemove) {
if (array_key_exists($parameterToRemove, $gridParameters)) {
unset($gridParameters[$parameterToRemove]);
}
}
return $gridParameters;
}
/**
* @param array|string|null $parameters
* @param array|string|null $removeParameters
* @return array
*/
public function getUrlGridParameters($parameters = null, $removeParameters = null)
{
$gridParameters = array_replace_recursive(
$this->getGridParameters($removeParameters),
(array)$parameters
);
return [self::GET_PARAMETER => [$this->getId() => $gridParameters]];
}
/**
* @param array|string|null $parameters
* @param array|string|null $removeParameters
* @return array
*/
public function getUrlParameters($parameters = null, $removeParameters = null)
{
return array_replace_recursive(
$this->requestStack->getMasterRequest()->query->all(),
$this->requestStack->getMasterRequest()->attributes->get('_route_params'),
$this->getUrlGridParameters($parameters, $removeParameters)
);
}
protected function loadRows()
{
if (array_key_exists($this->orderSourceColumnName, $this->columnsById)
&& $this->columnsById[$this->orderSourceColumnName]->isSortable()
) {
$orderSourceColumnName = $this->columnsById[$this->orderSourceColumnName]->getOrderSourceColumnName();
} else {
$orderSourceColumnName = null;
}
$orderDirection = $this->orderDirection;
if ($this->isDragAndDrop()) {
$orderSourceColumnName = null;
$orderDirection = null;
}
$this->paginationResults = $this->dataSource->getPaginatedRows(
$this->enablePaging ? $this->limit : null,
$this->page,
$orderSourceColumnName,
$orderDirection
);
$this->rows = $this->paginationResults->getResults();
}
/**
* @param int $rowId
*/
protected function loadRowsWithOneRow($rowId)
{
$this->rows = [$this->dataSource->getOneRow($rowId)];
}
protected function executeTotalQuery()
{
$this->totalCount = $this->dataSource->getTotalRowsCount();
$this->pageCount = max(ceil($this->totalCount / $this->limit), 1);
$this->page = min($this->page, $this->pageCount);
}
/**
* @param array $row
* @param string $sourceColumnName
* @return mixed
*/
public static function getValueFromRowBySourceColumnName(array $row, $sourceColumnName)
{
$sourceColumnNameParts = explode('.', $sourceColumnName);
if (count($sourceColumnNameParts) === 1) {
return $row[$sourceColumnNameParts[0]];
} elseif (count($sourceColumnNameParts) === 2) {
if (array_key_exists($sourceColumnNameParts[0], $row)
&& array_key_exists($sourceColumnNameParts[1], $row[$sourceColumnNameParts[0]])
) {
return $row[$sourceColumnNameParts[0]][$sourceColumnNameParts[1]];
} elseif (array_key_exists($sourceColumnNameParts[1], $row)) {
return $row[$sourceColumnNameParts[1]];
} else {
return $row[$sourceColumnName];
}
}
return $row[$sourceColumnName];
}
/**
* @param string $entityClass
*/
public function enableDragAndDrop($entityClass)
{
$this->orderingEntityClass = $entityClass;
}
public function enableMultipleDragAndDrop()
{
$this->multipleDragAndDrop = true;
}
/**
* @return bool
*/
public function isDragAndDrop()
{
return $this->orderingEntityClass !== null;
}
/**
* @return string|null
*/
public function getOrderingEntityClass()
{
return $this->orderingEntityClass;
}
/**
* @return bool
*/
public function isMultipleDragAndDrop()
{
return $this->multipleDragAndDrop;
}
}