Skip to main content

Sylius Grid Deep Dive

· 4 min read
Stephan Hochdörfer
Head of IT Business Operations

While developing the Sylius connector for Gally, I took a deep dive into the Sylius Grid component. I wanted to hook Gally into the Grid component so that I did not have to rebuild the grid handling logic in the code of the Gally bundle.

Diving deep into the Sylius GridBundle, I realized that some parts are flexible, and others are inflexible. But let's digest things a bit.

First, you can define your grid e.g. in YAML syntax like this:

sylius_grid:
grids:
my_grid:
driver:
name: doctrine/orm
options:
class: "%app.model.user.class%"
repository:
method: myCustomMethod
arguments:
id: resource.id
sorting:
name: asc
limits: [10, 25, 50, 100]
fields:
name:
type: string
label: Name
filters:
name:
type: string

This way, you can define the grid driver (usually this will be doctrine/orm—unless you want or need to build something on your own), configure the sorting, the fields to display, and the filters without writing any additional line of code.

I quickly realized that I had to implement a custom grid driver to solve the specific use case I wanted to solve. A grid driver is responsible for setting up and configuring the connection to the data source (e.g., database, REST endpoint, etc.) and returning the requested data when the getData() method is called.

The driver needs to implement the \Sylius\Component\Grid\Parameters\DataSourceInterface and needs to be exposed as a Symfony service tagged with the tag name sylius.grid_driver and an alias which is used in the Grid configuration as driver name:

<service id="BitExpert\MyPlugin\Grid\MyGrid\Driver">
<tag name="sylius.grid_driver" alias="my/rest" />
</service>

So, for our specific use case, the grid definition looks like this:

sylius_grid:
grids:
my_grid:
driver:
name: my/rest
options:
class: "%app.model.user.class%"
repository:
method: myCustomMethod
arguments:
id: resource.id

When the grid logic is called in code, the data flow works roughly this way:

The DataProvider::getData() method will fetch the data source implementation, configure filters and sorting options, and call the getData() method on your data source. Technically, you can return an array of data, but depending on the calling logic, Sylius may need a PagerFanta object to have access to all the data needed (e.g. current result page, number of results).

While building the custom Gally Grid datasource, I encountered a few problems that complicated building the adapter more than expected.

First, automatically adding filters and sorting by the default DataProvider implementation was a problem because the filtering and sorting are dynamically handled by the Gally server component. The only way to fix this was to overwrite the default Sylius Grid DataProvider implementation and add an exception for this particular use case like this:

final class DataProvider implements DataProviderInterface
{
// [...]

public function getData(Grid $grid, Parameters $parameters)
{
if ($grid->getCode() === 'sylius_shop_product') {
$channel = $this->channelContext->getChannel();
if (($channel instanceof GallyChannelInterface) && ($channel->getGallyActive())) {
$dataSource = $this->dataSourceProvider->getDataSource($grid, $parameters);

$this->filtersApplicator->apply($dataSource, $grid, $parameters);

return $dataSource->getData($parameters);
}
}

// by default use Sylius' implementation of the data provider
$dataProvider = new \Sylius\Component\Grid\Data\DataProvider(
$this->dataSourceProvider,
$this->filtersApplicator,
$this->sorter,
);
return $dataProvider->getData($grid, $parameters);
}
}

If the grid that should be rendered is the sylius_shop_product grid, I run my custom logic and omit the addition of filters and sorters. The code falls back to the default Sylius Grid DataProvider implementation for any other grid.

Second, since I had to return a PagerFanta object, I needed a custom PagerfantaGallyAdapter implementing the Pagerfanta\Adapter\AdapterInterface interface. Since I had to pass more context information to the Gally server (e.g. channel, taxon, locale, ...) than was passed via the Parameters method argument, I had to inject the entities as a service to my data source implementation and then manually pass the data to the custom Pagerfanta adapter. Even though it worked "fine", it initially felt a bit weird. Maybe it would be a good idea to have an event mechanism to populate the Parameters method argument with additional data.

I've learned a lot about Sylius architecture and how internal grid handling works. Although I have discovered some things to improve, I am impressed by the overall architecture and its flexibility.