Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Pole position on the grid

Pole position on the grid

Loïc and Estelle will guide you through the Sylius Grid, the powerful, flexible way to display and filter data in any Symfony application. In a deep dive that leaves eCommerce behind, they will show how data providers let you pull records from a database or an external API without altering your presentation layer. You will explore the brand‑new #[AsGrid] and #[AsFilter] PHP attributes that slash boilerplate and improve developer experience, then see a DataTable Component built on Symfony UX LiveComponent deliver reactive grid updates with zero JavaScript. Whether you maintain a complex admin panel or craft custom back‑office tools, you will be ready to harness the full potential of the Sylius Grid.

Avatar for Loïc Frémont

Loïc Frémont

October 20, 2025
Tweet

More Decks by Loïc Frémont

Other Decks in Programming

Transcript

  1. 🏎️ Pole position on the grid 🏁 Awesome new Sylius

    Grid features to put you in the driver’s seat
  2. 1. Akawaka & Sylius ❤️ 2. What’s under the hood

    of the Sylius Stack? 3. What if… you were Toto Wolff? 🏎️ 4. New Live Component Grid 🧟 5. Quick practice before it’s your time to shine! 6. Conclusion
  3. Akawaka & Sylius a true 💖 story Sylius Partner since

    2022 4 amazing Sylius Key Contributors : Loïc Frémont Valentin Silvestre Florian Merle Estelle Gaits
  4. What’s under the hood of the Sylius Stack? Create a

    lean & mean back-office in no time with :
  5. What’s under the hood of the Sylius Stack? Sylius Grid

    bundle Create a lean & mean back-office in no time with :
  6. What’s under the hood of the Sylius Stack? Sylius Grid

    bundle Sylius Resource bundle Create a lean & mean back-office in no time with :
  7. What’s under the hood of the Sylius Stack? Sylius Grid

    bundle Sylius Resource bundle Doctrine ORM & DBAL drivers Create a lean & mean back-office in no time with :
  8. What’s under the hood of the Sylius Stack? Sylius Grid

    bundle Sylius Resource bundle Doctrine ORM & DBAL drivers Providers/processors system Create a lean & mean back-office in no time with :
  9. What’s under the hood of the Sylius Stack? Sylius Grid

    bundle Sylius Resource bundle Doctrine ORM & DBAL drivers Providers/processors system Bootstrap Admin UI Create a lean & mean back-office in no time with :
  10. What’s under the hood of the Sylius Stack? Sylius Grid

    bundle Sylius Resource bundle Doctrine ORM & DBAL drivers Providers/processors system Bootstrap Admin UI Symfony UX, AssetMapper and more ! Create a lean & mean back-office in no time with :
  11. What if… you were Toto Wolff? 🏎️ the OpenF1 API

    the Sylius Stack boosted make:grid command new Sylius Grid Attributes #[AsGrid] #[AsFilter] Let’s create a Formula 1 admin panel using :
  12. What do we need to generate an index grid? routing

    & operations (Sylius Resource attributes) a data model (can be a Sylius Resource) a data provider (Doctrine, API repo, etc) a Sylius Grid definition (structure: fields, filters, sorting, pagination and actions)
  13. 1- Create a Resource #[AsResource( templatesDir: '@SyliusAdminUi/crud', operations: [ new

    Index(grid: DriverGrid::class), ], )] final readonly class DriverResource implements ResourceInterface { public function __construct( public int $number, public string $firstName, public string $lastName, public string $countryCode, public string $teamName, public string|null $image = null, ) { } public function getId(): int { return $this->number; } }
  14. 1- Create a Resource operations: [ new Index(grid: DriverGrid::class), ],

    #[AsResource( templatesDir: '@SyliusAdminUi/crud', )] final readonly class DriverResource implements ResourceInterface { public function __construct( public int $number, public string $firstName, public string $lastName, public string $countryCode, public string $teamName, public string|null $image = null, ) { } public function getId(): int { return $this->number; } }
  15. 1- Create a Resource #[AsResource( templatesDir: '@SyliusAdminUi/crud', operations: [ new

    Index(grid: DriverGrid::class), ], )] final readonly class DriverResource implements ResourceInterface { public function __construct( public int $number, public string $firstName, public string $lastName, public string $countryCode, public string $teamName, public string|null $image = null, ) { } public function getId(): int { return $this->number; } }
  16. 2- Create a Grid 💡 Now you can also create

    a grid based on a non-Doctrine entity, including… Sylius Resources !
  17. 2- Create a Grid 💡 Now you can also create

    a grid based on a non-Doctrine entity, including… Sylius Resources ! symfony console make:grid 'App\Resource\DriverResource'
  18. 2- Create a Grid 💡 Now you can also create

    a grid based on a non-Doctrine entity, including… Sylius Resources ! symfony console make:grid 'App\Resource\DriverResource'
  19. 2- Create a Grid #[AsGrid( resourceClass: DriverResource::class, name: 'app_driver_resource', )]

    final class DriverResourceGrid extends AbstractGrid { public function __construct() { // TODO inject services if required } public function __invoke(GridBuilderInterface $gridBuilder): void { $gridBuilder ->addField(StringField::create('firstName')->setLabel('FirstName')->setSortable(true)) // ... all fields are added except "id" ->addActionGroup(MainActionGroup::create( CreateAction::create() )) ->addActionGroup(BulkActionGroup::create( DeleteAction::create() )) ->addActionGroup(ItemActionGroup::create( // ShowAction::create(), UpdateAction::create(), DeleteAction::create()) ) ;
  20. 2- Create a Grid public function __invoke(GridBuilderInterface $gridBuilder): void {

    $gridBuilder ->addField(StringField::create('firstName')->setLabel('FirstName')->setSortable(true)) // ... all fields are added except "id" ->addActionGroup(MainActionGroup::create( CreateAction::create() )) ->addActionGroup(BulkActionGroup::create( DeleteAction::create() )) ->addActionGroup(ItemActionGroup::create( // ShowAction::create(), UpdateAction::create(), DeleteAction::create()) ) ; #[AsGrid( resourceClass: DriverResource::class, name: 'app_driver_resource', )] final class DriverResourceGrid extends AbstractGrid { public function __construct() { // TODO inject services if required }
  21. 2- Create a Grid #[AsGrid( resourceClass: DriverResource::class, name: 'app_driver_resource', )]

    final class DriverResourceGrid extends AbstractGrid { public function __construct() { // TODO inject services if required } public function __invoke(GridBuilderInterface $gridBuilder): void { $gridBuilder ->addField(StringField::create('firstName')->setLabel('FirstName')->setSortable(true)) // ... all fields are added except "id" ->addActionGroup(MainActionGroup::create( CreateAction::create() )) ->addActionGroup(BulkActionGroup::create( DeleteAction::create() )) ->addActionGroup(ItemActionGroup::create( // ShowAction::create(), UpdateAction::create(), DeleteAction::create()) ) ;
  22. 4- Create a custom Grid Data Provider 👶 3 Baby

    steps 1. Without any data 2. With hardcoded data
  23. 4- Create a custom Grid Data Provider 👶 3 Baby

    steps 1. Without any data 2. With hardcoded data 3. With data from the F1 API
  24. 1. Without any data namespace App\Grid; use Pagerfanta\Adapter\FixedAdapter; use Pagerfanta\Pagerfanta;

    use Pagerfanta\PagerfantaInterface; use Sylius\Component\Grid\Data\DataProviderInterface; use Sylius\Component\Grid\Definition\Grid; use Sylius\Component\Grid\Parameters; final readonly class DriverGridProvider implements DataProviderInterface { public function getData(Grid $grid, Parameters $parameters): PagerFantaInterface { // start with an empty paginator return new Pagerfanta(new FixedAdapter(0, [])); } }
  25. 1. Without any data final readonly class DriverGridProvider implements DataProviderInterface

    namespace App\Grid; use Pagerfanta\Adapter\FixedAdapter; use Pagerfanta\Pagerfanta; use Pagerfanta\PagerfantaInterface; use Sylius\Component\Grid\Data\DataProviderInterface; use Sylius\Component\Grid\Definition\Grid; use Sylius\Component\Grid\Parameters; { public function getData(Grid $grid, Parameters $parameters): PagerFantaInterface { // start with an empty paginator return new Pagerfanta(new FixedAdapter(0, [])); } }
  26. 1. Without any data use Sylius\Component\Grid\Data\DataProviderInterface; final readonly class DriverGridProvider

    implements DataProviderInterface namespace App\Grid; use Pagerfanta\Adapter\FixedAdapter; use Pagerfanta\Pagerfanta; use Pagerfanta\PagerfantaInterface; use Sylius\Component\Grid\Definition\Grid; use Sylius\Component\Grid\Parameters; { public function getData(Grid $grid, Parameters $parameters): PagerFantaInterface { // start with an empty paginator return new Pagerfanta(new FixedAdapter(0, [])); } }
  27. 1. Without any data public function getData(Grid $grid, Parameters $parameters):

    PagerFantaInterface namespace App\Grid; use Pagerfanta\Adapter\FixedAdapter; use Pagerfanta\Pagerfanta; use Pagerfanta\PagerfantaInterface; use Sylius\Component\Grid\Data\DataProviderInterface; use Sylius\Component\Grid\Definition\Grid; use Sylius\Component\Grid\Parameters; final readonly class DriverGridProvider implements DataProviderInterface { { // start with an empty paginator return new Pagerfanta(new FixedAdapter(0, [])); } }
  28. 1. Without any data use Pagerfanta\PagerfantaInterface; public function getData(Grid $grid,

    Parameters $parameters): PagerFantaInterface namespace App\Grid; use Pagerfanta\Adapter\FixedAdapter; use Pagerfanta\Pagerfanta; use Sylius\Component\Grid\Data\DataProviderInterface; use Sylius\Component\Grid\Definition\Grid; use Sylius\Component\Grid\Parameters; final readonly class DriverGridProvider implements DataProviderInterface { { // start with an empty paginator return new Pagerfanta(new FixedAdapter(0, [])); } }
  29. 1. Without any data // start with an empty paginator

    return new Pagerfanta(new FixedAdapter(0, [])); namespace App\Grid; use Pagerfanta\Adapter\FixedAdapter; use Pagerfanta\Pagerfanta; use Pagerfanta\PagerfantaInterface; use Sylius\Component\Grid\Data\DataProviderInterface; use Sylius\Component\Grid\Definition\Grid; use Sylius\Component\Grid\Parameters; final readonly class DriverGridProvider implements DataProviderInterface { public function getData(Grid $grid, Parameters $parameters): PagerFantaInterface { } }
  30. 2. Use hardcoded data // ... final readonly class DriverGridProvider

    implements DataProviderInterface { public function getData(Grid $grid, Parameters $parameters): PagerFantaInterface { // start with a fixed data paginator return new Pagerfanta(new FixedAdapter(4, $this->getDrivers())); } private function getDrivers(): iterable { yield new DriverResource(number: 1, firstName: 'Max', lastName: 'Verstappen', countryCode: 'NED', teamName: 'Red Bu yield new DriverResource(number: 2, firstName: 'Logan', lastName: 'Sargeant', countryCode: 'USA', teamName: 'Willia yield new DriverResource(number: 4, firstName: 'Lando', lastName: 'Norris', countryCode: 'GBR', teamName: 'McLaren' yield new DriverResource(number: 44, firstName: 'Lewis', lastName: 'Hamilton', countryCode: 'GBR', teamName: 'Merce } }
  31. 2. Use hardcoded data // start with a fixed data

    paginator return new Pagerfanta(new FixedAdapter(4, $this->getDrivers())); // ... final readonly class DriverGridProvider implements DataProviderInterface { public function getData(Grid $grid, Parameters $parameters): PagerFantaInterface { } private function getDrivers(): iterable { yield new DriverResource(number: 1, firstName: 'Max', lastName: 'Verstappen', countryCode: 'NED', teamName: 'Red Bu yield new DriverResource(number: 2, firstName: 'Logan', lastName: 'Sargeant', countryCode: 'USA', teamName: 'Willia yield new DriverResource(number: 4, firstName: 'Lando', lastName: 'Norris', countryCode: 'GBR', teamName: 'McLaren' yield new DriverResource(number: 44, firstName: 'Lewis', lastName: 'Hamilton', countryCode: 'GBR', teamName: 'Merce } }
  32. 2. Use hardcoded data private function getDrivers(): iterable { yield

    new DriverResource(number: 1, firstName: 'Max', lastName: 'Verstappen', countryCode: 'NED', teamName: 'Red Bu yield new DriverResource(number: 2, firstName: 'Logan', lastName: 'Sargeant', countryCode: 'USA', teamName: 'Willia yield new DriverResource(number: 4, firstName: 'Lando', lastName: 'Norris', countryCode: 'GBR', teamName: 'McLaren' yield new DriverResource(number: 44, firstName: 'Lewis', lastName: 'Hamilton', countryCode: 'GBR', teamName: 'Merce } // ... final readonly class DriverGridProvider implements DataProviderInterface { public function getData(Grid $grid, Parameters $parameters): PagerFantaInterface { // start with a fixed data paginator return new Pagerfanta(new FixedAdapter(4, $this->getDrivers())); } }
  33. namespace App\Grid\Provider; use Pagerfanta\Adapter\ArrayAdapter; use Pagerfanta\Pagerfanta; use Pagerfanta\PagerfantaInterface; use Sylius\Component\Grid\Data\DataProviderInterface;

    use Sylius\Component\Grid\Definition\Grid; use Sylius\Component\Grid\Parameters; use Symfony\Contracts\HttpClient\HttpClientInterface; final readonly class DriverGridProvider implements DataProviderInterface { public function __construct( private HttpClientInterface $openF1Client, ) {} public function getData(Grid $grid, Parameters $parameters): PagerFantaInterface { return new Pagerfanta(new ArrayAdapter(iterator_to_array($this->getDrivers()))); } private function getDrivers(): iterable { // ... } }
  34. use Symfony\Contracts\HttpClient\HttpClientInterface; private HttpClientInterface $openF1Client, namespace App\Grid\Provider; use Pagerfanta\Adapter\ArrayAdapter; use

    Pagerfanta\Pagerfanta; use Pagerfanta\PagerfantaInterface; use Sylius\Component\Grid\Data\DataProviderInterface; use Sylius\Component\Grid\Definition\Grid; use Sylius\Component\Grid\Parameters; final readonly class DriverGridProvider implements DataProviderInterface { public function __construct( ) {} public function getData(Grid $grid, Parameters $parameters): PagerFantaInterface { return new Pagerfanta(new ArrayAdapter(iterator_to_array($this->getDrivers()))); } private function getDrivers(): iterable { // ... } }
  35. public function getData(Grid $grid, Parameters $parameters): PagerFantaInterface { return new

    Pagerfanta(new ArrayAdapter(iterator_to_array($this->getDrivers()))); } namespace App\Grid\Provider; use Pagerfanta\Adapter\ArrayAdapter; use Pagerfanta\Pagerfanta; use Pagerfanta\PagerfantaInterface; use Sylius\Component\Grid\Data\DataProviderInterface; use Sylius\Component\Grid\Definition\Grid; use Sylius\Component\Grid\Parameters; use Symfony\Contracts\HttpClient\HttpClientInterface; final readonly class DriverGridProvider implements DataProviderInterface { public function __construct( private HttpClientInterface $openF1Client, ) {} private function getDrivers(): iterable { // ... } }
  36. private function getDrivers(): iterable { // ... } namespace App\Grid\Provider;

    use Pagerfanta\Adapter\ArrayAdapter; use Pagerfanta\Pagerfanta; use Pagerfanta\PagerfantaInterface; use Sylius\Component\Grid\Data\DataProviderInterface; use Sylius\Component\Grid\Definition\Grid; use Sylius\Component\Grid\Parameters; use Symfony\Contracts\HttpClient\HttpClientInterface; final readonly class DriverGridProvider implements DataProviderInterface { public function __construct( private HttpClientInterface $openF1Client ) {} // ... }
  37. use Sylius\Component\Grid\Data\DataProviderInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; final readonly class DriverGridProvider implements DataProviderInterface

    { // ... private function getDrivers(): iterable { $responseData = $this->openF1Client->request('GET', '/v1/drivers?session_key=9158')->toArray(); foreach ($responseData as $row) { yield new DriverResource( number: $row['driver_number'], firstName: $row['first_name'], lastName: $row['last_name'], countryCode: $row['country_code'], teamName: $row['team_name'], image: $row['headshot_url'], ); } } }
  38. $responseData = $this->openF1Client->request('GET', '/v1/drivers?session_key=9158')->toArray(); foreach ($responseData as $row) { yield

    new DriverResource( number: $row['driver_number'], firstName: $row['first_name'], lastName: $row['last_name'], countryCode: $row['country_code'], teamName: $row['team_name'], image: $row['headshot_url'], ); } use Sylius\Component\Grid\Data\DataProviderInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; final readonly class DriverGridProvider implements DataProviderInterface { // ... private function getDrivers(): iterable { } }
  39. Resource class & provider arguments use App\Entity\Meeting; use Sylius\Bundle\GridBundle\Grid\AbstractGrid; #[AsGrid(

    provider: MeetingGridProvider::class, resourceClass: Meeting::class, // any PHP object, including a Sylius resource )] final class MeetingGrid extends AbstractGrid { public function __invoke( GridBuilderInterface $gridBuilder, ): void // ... } }
  40. Resource class & provider arguments provider: MeetingGridProvider::class, use App\Entity\Meeting; use

    Sylius\Bundle\GridBundle\Grid\AbstractGrid; #[AsGrid( resourceClass: Meeting::class, // any PHP object, including a Sylius resource )] final class MeetingGrid extends AbstractGrid { public function __invoke( GridBuilderInterface $gridBuilder, ): void // ... } }
  41. Resource class & provider arguments resourceClass: Meeting::class, // any PHP

    object, including a Sylius resource use App\Entity\Meeting; use Sylius\Bundle\GridBundle\Grid\AbstractGrid; #[AsGrid( provider: MeetingGridProvider::class, )] final class MeetingGrid extends AbstractGrid { public function __invoke( GridBuilderInterface $gridBuilder, ): void // ... } }
  42. Resource class & provider arguments public function __invoke( GridBuilderInterface $gridBuilder,

    ): void // ... } use App\Entity\Meeting; use Sylius\Bundle\GridBundle\Grid\AbstractGrid; #[AsGrid( provider: MeetingGridProvider::class, resourceClass: Meeting::class, // any PHP object, including a Sylius resource )] final class MeetingGrid extends AbstractGrid { }
  43. Resource class & provider arguments use App\Entity\Meeting; use Sylius\Bundle\GridBundle\Grid\AbstractGrid; #[AsGrid(

    provider: MeetingGridProvider::class, resourceClass: Meeting::class, // any PHP object, including a Sylius resource )] final class MeetingGrid extends AbstractGrid { public function __invoke( GridBuilderInterface $gridBuilder, ): void // ... } }
  44. build method and name arguments use Sylius\Bundle\GridBundle\Grid\AbstractGrid; #[AsGrid( buildMethod: 'customBuildMethod',

    name: 'meeting', // optional - FQCN by default // ... )] final class MeetingGrid extends AbstractGrid { public function customBuildMethod( GridBuilderInterface $gridBuilder, ): void // ... } }
  45. build method and name arguments buildMethod: 'customBuildMethod', public function customBuildMethod(

    use Sylius\Bundle\GridBundle\Grid\AbstractGrid; #[AsGrid( name: 'meeting', // optional - FQCN by default // ... )] final class MeetingGrid extends AbstractGrid { GridBuilderInterface $gridBuilder, ): void // ... } }
  46. build method and name arguments name: 'meeting', // optional -

    FQCN by default use Sylius\Bundle\GridBundle\Grid\AbstractGrid; #[AsGrid( buildMethod: 'customBuildMethod', // ... )] final class MeetingGrid extends AbstractGrid { public function customBuildMethod( GridBuilderInterface $gridBuilder, ): void // ... } }
  47. build method and name arguments use Sylius\Bundle\GridBundle\Grid\AbstractGrid; #[AsGrid( buildMethod: 'customBuildMethod',

    name: 'meeting', // optional - FQCN by default // ... )] final class MeetingGrid extends AbstractGrid { public function customBuildMethod( GridBuilderInterface $gridBuilder, ): void // ... } }
  48. [#AsGrid] __invoke() instead of buildGrid() - but buildGrid still supported

    by default if no __invoke() buildMethod argument to declare custom build method
  49. [#AsGrid] __invoke() instead of buildGrid() - but buildGrid still supported

    by default if no __invoke() buildMethod argument to declare custom build method provider argument instead of ->setProvider()
  50. [#AsGrid] __invoke() instead of buildGrid() - but buildGrid still supported

    by default if no __invoke() buildMethod argument to declare custom build method provider argument instead of ->setProvider() resourceClass argument instead of getResourceClass()
  51. [#AsGrid] __invoke() instead of buildGrid() - but buildGrid still supported

    by default if no __invoke() buildMethod argument to declare custom build method provider argument instead of ->setProvider() resourceClass argument instead of getResourceClass() name argument (FQCN by default) instead of getName()
  52. #[AsFilter] Creating your own filter type namespace App\Grid\Filter; use Sylius\Component\Grid\Attribute\AsFilter;

    use Sylius\Component\Grid\Data\DataSourceInterface; use Sylius\Component\Grid\Filtering\FilterInterface; use Symfony\Component\Form\Extension\Core\Type\CountryType; #[AsFilter( formType: CountryType::class, // Symfony Form Type to use template: '@SyliusBootstrapAdminUi/shared/grid/filter/select.html.twig', // The Twig template )] final class CountryFilter implements FilterInterface { public function apply(DataSourceInterface $dataSource, string $name, $data, array $options): void { // We handle the filtering part in the DriverGridProvider } }
  53. #[AsFilter] Creating your own filter type use Sylius\Component\Grid\Attribute\AsFilter; #[AsFilter( namespace

    App\Grid\Filter; use Sylius\Component\Grid\Data\DataSourceInterface; use Sylius\Component\Grid\Filtering\FilterInterface; use Symfony\Component\Form\Extension\Core\Type\CountryType; formType: CountryType::class, // Symfony Form Type to use template: '@SyliusBootstrapAdminUi/shared/grid/filter/select.html.twig', // The Twig template )] final class CountryFilter implements FilterInterface { public function apply(DataSourceInterface $dataSource, string $name, $data, array $options): void { // We handle the filtering part in the DriverGridProvider } }
  54. #[AsFilter] Creating your own filter type use Symfony\Component\Form\Extension\Core\Type\CountryType; formType: CountryType::class,

    // Symfony Form Type to use namespace App\Grid\Filter; use Sylius\Component\Grid\Attribute\AsFilter; use Sylius\Component\Grid\Data\DataSourceInterface; use Sylius\Component\Grid\Filtering\FilterInterface; #[AsFilter( template: '@SyliusBootstrapAdminUi/shared/grid/filter/select.html.twig', // The Twig template )] final class CountryFilter implements FilterInterface { public function apply(DataSourceInterface $dataSource, string $name, $data, array $options): void { // We handle the filtering part in the DriverGridProvider } }
  55. #[AsFilter] Creating your own filter type template: '@SyliusBootstrapAdminUi/shared/grid/filter/select.html.twig', // The

    Twig template namespace App\Grid\Filter; use Sylius\Component\Grid\Attribute\AsFilter; use Sylius\Component\Grid\Data\DataSourceInterface; use Sylius\Component\Grid\Filtering\FilterInterface; use Symfony\Component\Form\Extension\Core\Type\CountryType; #[AsFilter( formType: CountryType::class, // Symfony Form Type to use )] final class CountryFilter implements FilterInterface { public function apply(DataSourceInterface $dataSource, string $name, $data, array $options): void { // We handle the filtering part in the DriverGridProvider } }
  56. #[AsFilter] Creating your own filter type use Sylius\Component\Grid\Filtering\FilterInterface; final class

    CountryFilter implements FilterInterface { public function apply(DataSourceInterface $dataSource, string $name, $data, array $options): void { // We handle the filtering part in the DriverGridProvider } } namespace App\Grid\Filter; use Sylius\Component\Grid\Attribute\AsFilter; use Sylius\Component\Grid\Data\DataSourceInterface; use Symfony\Component\Form\Extension\Core\Type\CountryType; #[AsFilter( formType: CountryType::class, // Symfony Form Type to use template: '@SyliusBootstrapAdminUi/shared/grid/filter/select.html.twig', // The Twig template )]
  57. #[AsFilter] Creating your own filter type namespace App\Grid\Filter; use Sylius\Component\Grid\Attribute\AsFilter;

    use Sylius\Component\Grid\Data\DataSourceInterface; use Sylius\Component\Grid\Filtering\FilterInterface; use Symfony\Component\Form\Extension\Core\Type\CountryType; #[AsFilter( formType: CountryType::class, // Symfony Form Type to use template: '@SyliusBootstrapAdminUi/shared/grid/filter/select.html.twig', // The Twig template )] final class CountryFilter implements FilterInterface { public function apply(DataSourceInterface $dataSource, string $name, $data, array $options): void { // We handle the filtering part in the DriverGridProvider } }
  58. Insert the filter into our Grid #[AsGrid] final class DriverGrid

    extends AbstractGrid { public function buildGrid(GridBuilderInterface $gridBuilder): void { $gridBuilder ->addFilter( Filter::create('country', CountryFilter::class) ->setFormOptions([ 'alpha3' => true, 'autocomplete' => true, ]) ->setLabel('app.ui.country') ) // ... ; } }
  59. Insert the filter into our Grid ->addFilter( ) #[AsGrid] final

    class DriverGrid extends AbstractGrid { public function buildGrid(GridBuilderInterface $gridBuilder): void { $gridBuilder Filter::create('country', CountryFilter::class) ->setFormOptions([ 'alpha3' => true, 'autocomplete' => true, ]) ->setLabel('app.ui.country') // ... ; } }
  60. Insert the filter into our Grid Filter::create('country', CountryFilter::class) ->setFormOptions([ 'alpha3'

    => true, 'autocomplete' => true, ]) #[AsGrid] final class DriverGrid extends AbstractGrid { public function buildGrid(GridBuilderInterface $gridBuilder): void { $gridBuilder ->addFilter( ->setLabel('app.ui.country') ) // ... ; } }
  61. Insert the filter into our Grid #[AsGrid] final class DriverGrid

    extends AbstractGrid { public function buildGrid(GridBuilderInterface $gridBuilder): void { $gridBuilder ->addFilter( Filter::create('country', CountryFilter::class) ->setFormOptions([ 'alpha3' => true, 'autocomplete' => true, ]) ->setLabel('app.ui.country') ) // ... ; } }
  62. Actual filtering logic inside the provider final readonly class DriverApiGridProvider

    implements DataProviderInterface { // ... private function getDrivers(array $criteria): iterable { $query = ['session_key' => 9158]; if (!empty($criteria['country'] ?? null)) { $query['country_code'] = $criteria['country']; } $responseData = $this->openF1Client ->request(method: 'GET', url: '/v1/drivers', options: ['query' => $query]) ->toArray() ; foreach ($responseData as $row) { yield new DriverResource( number: $row['driver_number'], // ... ); } } }
  63. Actual filtering logic inside the provider if (!empty($criteria['country'] ?? null))

    { $query['country_code'] = $criteria['country']; } final readonly class DriverApiGridProvider implements DataProviderInterface { // ... private function getDrivers(array $criteria): iterable { $query = ['session_key' => 9158]; $responseData = $this->openF1Client ->request(method: 'GET', url: '/v1/drivers', options: ['query' => $query]) ->toArray() ; foreach ($responseData as $row) { yield new DriverResource( number: $row['driver_number'], // ... ); } } }
  64. Actual filtering logic inside the provider ->request(method: 'GET', url: '/v1/drivers',

    options: ['query' => $query]) final readonly class DriverApiGridProvider implements DataProviderInterface { // ... private function getDrivers(array $criteria): iterable { $query = ['session_key' => 9158]; if (!empty($criteria['country'] ?? null)) { $query['country_code'] = $criteria['country']; } $responseData = $this->openF1Client ->toArray() ; foreach ($responseData as $row) { yield new DriverResource( number: $row['driver_number'], // ... ); } } }
  65. Actual filtering logic inside the provider yield new DriverResource( number:

    $row['driver_number'], // ... ); final readonly class DriverApiGridProvider implements DataProviderInterface { // ... private function getDrivers(array $criteria): iterable { $query = ['session_key' => 9158]; if (!empty($criteria['country'] ?? null)) { $query['country_code'] = $criteria['country']; } $responseData = $this->openF1Client ->request(method: 'GET', url: '/v1/drivers', options: ['query' => $query]) ->toArray() ; foreach ($responseData as $row) { } } }
  66. Actual filtering logic inside the provider final readonly class DriverApiGridProvider

    implements DataProviderInterface { // ... private function getDrivers(array $criteria): iterable { $query = ['session_key' => 9158]; if (!empty($criteria['country'] ?? null)) { $query['country_code'] = $criteria['country']; } $responseData = $this->openF1Client ->request(method: 'GET', url: '/v1/drivers', options: ['query' => $query]) ->toArray() ; foreach ($responseData as $row) { yield new DriverResource( number: $row['driver_number'], // ... ); } } }
  67. [#AsFilter] formType argument instead of getFormType() template argument instead of

    defining in config/packages/sylius_grid.php type argument for the name of your custom filter type (FQCN by default) default ones are string , boolean , money , date …
  68. namespace App\Grid; use Sylius\Bundle\GridBundle\Builder\Filter\StringFilter; use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface; use Sylius\Bundle\GridBundle\Grid\AbstractGrid; use Sylius\Component\Grid\Attribute\AsGrid;

    #[AsGrid] final class TeamRadioGrid extends AbstractGrid { public function buildGrid(GridBuilderInterface $gridBuilder): void { $gridBuilder // ... ->addFilter( StringFilter::create(name: 'driver_number', type: 'equal') ->setLabel('app.ui.driver_number') ) // ... ; } }
  69. StringFilter::create(name: 'driver_number', type: 'equal') ->setLabel('app.ui.driver_number') namespace App\Grid; use Sylius\Bundle\GridBundle\Builder\Filter\StringFilter; use

    Sylius\Bundle\GridBundle\Builder\GridBuilderInterface; use Sylius\Bundle\GridBundle\Grid\AbstractGrid; use Sylius\Component\Grid\Attribute\AsGrid; #[AsGrid] final class TeamRadioGrid extends AbstractGrid { public function buildGrid(GridBuilderInterface $gridBuilder): void { $gridBuilder // ... ->addFilter( ) // ... ; } }
  70. namespace App\Grid; use Sylius\Bundle\GridBundle\Builder\Filter\StringFilter; use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface; use Sylius\Bundle\GridBundle\Grid\AbstractGrid; use Sylius\Component\Grid\Attribute\AsGrid;

    #[AsGrid] final class TeamRadioGrid extends AbstractGrid { public function buildGrid(GridBuilderInterface $gridBuilder): void { $gridBuilder // ... ->addFilter( StringFilter::create(name: 'driver_number', type: 'equal') ->setLabel('app.ui.driver_number') ) // ... ; } }
  71. use Sylius\Bundle\GridBundle\Builder\Action\Action; use Sylius\Bundle\GridBundle\Builder\ActionGroup\ItemActionGroup; #[AsGrid] final class DriverGrid extends AbstractGrid

    { public function buildGrid(GridBuilderInterface $gridBuilder): void { $gridBuilder ->addActionGroup( ItemActionGroup::create( Action::create('team_radios', 'show') ->setOptions([ 'link' => [ 'route' => 'app_admin_team_radio_index', 'parameters' => [ 'criteria' => [ 'driver_number' => [ 'value' => 'resource.number', // driverResource->number ], ], ], ], ]) ->setLabel('app.ui.show_team_radios') ->setIcon('tabler:radio'), )
  72. use Sylius\Bundle\GridBundle\Builder\Action\Action; Action::create('team_radios', 'show') use Sylius\Bundle\GridBundle\Builder\ActionGroup\ItemActionGroup; #[AsGrid] final class DriverGrid

    extends AbstractGrid { public function buildGrid(GridBuilderInterface $gridBuilder): void { $gridBuilder ->addActionGroup( ItemActionGroup::create( ->setOptions([ 'link' => [ 'route' => 'app_admin_team_radio_index', 'parameters' => [ 'criteria' => [ 'driver_number' => [ 'value' => 'resource.number', // driverResource->number ], ], ], ], ]) ->setLabel('app.ui.show_team_radios') ->setIcon('tabler:radio'), )
  73. ->setOptions([ 'link' => [ 'route' => 'app_admin_team_radio_index', 'parameters' => [

    'criteria' => [ 'driver_number' => [ 'value' => 'resource.number', // driverResource->number ], ], ], ], ]) use Sylius\Bundle\GridBundle\Builder\Action\Action; use Sylius\Bundle\GridBundle\Builder\ActionGroup\ItemActionGroup; #[AsGrid] final class DriverGrid extends AbstractGrid { public function buildGrid(GridBuilderInterface $gridBuilder): void { $gridBuilder ->addActionGroup( ItemActionGroup::create( Action::create('team_radios', 'show') ->setLabel('app.ui.show_team_radios') ->setIcon('tabler:radio'), )
  74. Twig hooks overview index operation Index template Hook 'index' Sidebar

    Navbar Content Hook 'content' Flashes Header Grid
  75. Twig hooks overview index operation Index template Hook 'index' Sidebar

    Navbar Content Hook 'content' Flashes Header Grid
  76. Twig hooks overview index operation Index template Hook 'index' Sidebar

    Navbar Content Hook 'content' Flashes Header Grid
  77. Overview of the new DataTableComponent Transform your grid into a

    Live Component #[AsLiveComponent(name: 'sylius:grid:data_table')] final class DataTableComponent { #[LiveProp(writable: true)] public string|null $grid = null; #[LiveProp(writable: true)] public int $page = 1; #[LiveProp(writable: true)] public array|null $criteria = null; #[LiveProp(writable: true)] public array|null $sorting = null; #[LiveProp(writable: true)] public int|null $limit = null; } sylius_twig_hooks: hooks: 'sylius_admin.grid.index.content.grid': data_table: component: 'sylius:grid:data_table' props: grid: '@=_context.grid' page: '@=_context.page' criteria: '@=_context.criteria' sorting: '@=_context.sorting' limit: '@=_context.limit'
  78. Overview of the new DataTableComponent Transform your grid into a

    Live Component public string|null $grid = null; public int $page = 1; public array|null $criteria = null; public array|null $sorting = null; public int|null $limit = null; #[AsLiveComponent(name: 'sylius:grid:data_table')] final class DataTableComponent { #[LiveProp(writable: true)] #[LiveProp(writable: true)] #[LiveProp(writable: true)] #[LiveProp(writable: true)] #[LiveProp(writable: true)] } sylius_twig_hooks: hooks: 'sylius_admin.grid.index.content.grid': data_table: component: 'sylius:grid:data_table' props: grid: '@=_context.grid' page: '@=_context.page' criteria: '@=_context.criteria' sorting: '@=_context.sorting' limit: '@=_context.limit'
  79. Overview of the new DataTableComponent Transform your grid into a

    Live Component public string|null $grid = null; public int $page = 1; public array|null $criteria = null; public array|null $sorting = null; public int|null $limit = null; #[AsLiveComponent(name: 'sylius:grid:data_table')] final class DataTableComponent { #[LiveProp(writable: true)] #[LiveProp(writable: true)] #[LiveProp(writable: true)] #[LiveProp(writable: true)] #[LiveProp(writable: true)] } sylius_twig_hooks: hooks: 'sylius_admin.grid.index.content.grid': data_table: component: 'sylius:grid:data_table' props: grid: '@=_context.grid' page: '@=_context.page' criteria: '@=_context.criteria' sorting: '@=_context.sorting' limit: '@=_context.limit'
  80. Overview of the new DataTableComponent Transform your grid into a

    Live Component public string|null $grid = null; public int $page = 1; public array|null $criteria = null; public array|null $sorting = null; public int|null $limit = null; #[AsLiveComponent(name: 'sylius:grid:data_table')] final class DataTableComponent { #[LiveProp(writable: true)] #[LiveProp(writable: true)] #[LiveProp(writable: true)] #[LiveProp(writable: true)] #[LiveProp(writable: true)] } 'sylius_admin.grid.index.content.grid': sylius_twig_hooks: hooks: data_table: component: 'sylius:grid:data_table' props: grid: '@=_context.grid' page: '@=_context.page' criteria: '@=_context.criteria' sorting: '@=_context.sorting' limit: '@=_context.limit'
  81. Overview of the new DataTableComponent Transform your grid into a

    Live Component public string|null $grid = null; public int $page = 1; public array|null $criteria = null; public array|null $sorting = null; public int|null $limit = null; #[AsLiveComponent(name: 'sylius:grid:data_table')] final class DataTableComponent { #[LiveProp(writable: true)] #[LiveProp(writable: true)] #[LiveProp(writable: true)] #[LiveProp(writable: true)] #[LiveProp(writable: true)] } data_table: sylius_twig_hooks: hooks: 'sylius_admin.grid.index.content.grid': component: 'sylius:grid:data_table' props: grid: '@=_context.grid' page: '@=_context.page' criteria: '@=_context.criteria' sorting: '@=_context.sorting' limit: '@=_context.limit'
  82. Overview of the new DataTableComponent Transform your grid into a

    Live Component public string|null $grid = null; public int $page = 1; public array|null $criteria = null; public array|null $sorting = null; public int|null $limit = null; #[AsLiveComponent(name: 'sylius:grid:data_table')] final class DataTableComponent { #[LiveProp(writable: true)] #[LiveProp(writable: true)] #[LiveProp(writable: true)] #[LiveProp(writable: true)] #[LiveProp(writable: true)] } component: 'sylius:grid:data_table' sylius_twig_hooks: hooks: 'sylius_admin.grid.index.content.grid': data_table: props: grid: '@=_context.grid' page: '@=_context.page' criteria: '@=_context.criteria' sorting: '@=_context.sorting' limit: '@=_context.limit'
  83. Overview of the new DataTableComponent Transform your grid into a

    Live Component public string|null $grid = null; public int $page = 1; public array|null $criteria = null; public array|null $sorting = null; public int|null $limit = null; #[AsLiveComponent(name: 'sylius:grid:data_table')] final class DataTableComponent { #[LiveProp(writable: true)] #[LiveProp(writable: true)] #[LiveProp(writable: true)] #[LiveProp(writable: true)] #[LiveProp(writable: true)] } props: grid: '@=_context.grid' page: '@=_context.page' criteria: '@=_context.criteria' sorting: '@=_context.sorting' limit: '@=_context.limit' sylius_twig_hooks: hooks: 'sylius_admin.grid.index.content.grid': data_table: component: 'sylius:grid:data_table'
  84. Use it in any template Including your grid in a

    details page. <!-- templates/session/show/body.html.twig --> {{ component('sylius_grid_data_table', { grid: 'driver', criteria: { session: session.id, }, }) }}
  85. Use it in any template Including your grid in a

    details page. grid: 'driver', <!-- templates/session/show/body.html.twig --> {{ component('sylius_grid_data_table', { criteria: { session: session.id, }, }) }}
  86. Use it in any template Including your grid in a

    details page. criteria: { session: session.id, }, <!-- templates/session/show/body.html.twig --> {{ component('sylius_grid_data_table', { grid: 'driver', }) }}
  87. Live Component Filters final class SelectFilterComponent { #[LiveProp(writable: true)] public

    string|null $selectedValue = null; // selected value of the select input } final class StringFilterComponent { #[LiveProp(writable: true)] public string|null $value = null; // query string of the text input } final class DateFilterComponent { #[LiveProp(writable: true)] public string|null $fromDate = null; #[LiveProp(writable: true)] public string|null $fromTime = null; #[LiveProp(writable: true)] public string|null $toDate = null; #[LiveProp(writable: true)] public string|null $toTime = null;
  88. Quick practice before it’s your time to shine! #[AsFilter] #[AsGrid]

    #[AsProvider] #[AsResource] Q1: Find the odd one out.
  89. Quick practice before it’s your time to shine! #[AsFilter] #[AsGrid]

    #[AsProvider] #[AsResource] Q1: Find the odd one out.
  90. Q2: Which team did Max Verstappen race for when he

    won his first Formula One Grand Prix?
  91. Q2: Which team did Max Verstappen race for when he

    won his first Formula One Grand Prix? Ferrari
  92. Q2: Which team did Max Verstappen race for when he

    won his first Formula One Grand Prix? Ferrari Mercedes
  93. Q2: Which team did Max Verstappen race for when he

    won his first Formula One Grand Prix? Ferrari Mercedes Red Bull Racing
  94. Q2: Which team did Max Verstappen race for when he

    won his first Formula One Grand Prix? Ferrari Mercedes Red Bull Racing Scuderia Toro Rosso
  95. Q2: Which team did Max Verstappen race for when he

    won his first Formula One Grand Prix? Ferrari Mercedes Red Bull Racing Scuderia Toro Rosso
  96. Q3: The OpenF1 API does not include a /teams endpoint,

    but you would like to create a grid listing all teams. You’ve created this simple model … : final readonly class Team { public function __construct( public string $id, public string $name, public string|null $color = null, ) { } }
  97. Q3: What command could you run to generate the missing

    grid? symfony console create:grid 'App\Model\Team'
  98. Q3: What command could you run to generate the missing

    grid? symfony console create:grid 'App\Model\Team' symfony console make:resource 'App\Model\Team'
  99. Q3: What command could you run to generate the missing

    grid? symfony console create:grid 'App\Model\Team' symfony console make:resource 'App\Model\Team' symfony console make:grid 'App\Model\Team'
  100. Q3: What command could you run to generate the missing

    grid? symfony console create:grid 'App\Model\Team' symfony console make:resource 'App\Model\Team' symfony console make:grid 'App\Model\Team' symfony console make:grid 'App\Resource\TeamResource'
  101. Q3: What command could you run to generate the missing

    grid? symfony console create:grid 'App\Model\Team' symfony console make:resource 'App\Model\Team' symfony console make:grid 'App\Model\Team' symfony console make:grid 'App\Resource\TeamResource'
  102. Q4: How can you create a Team filter to use

    it on the F1 drivers’ grid? We’ve added the following filter on the grid configuration: Now, what should our filter look like? ->addFilter(Filter::create('teamName', TeamFilter::class))
  103. Q4: How can you create a Team filter to use

    it on the F1 drivers’ grid? We’ve added the following filter on the grid configuration: Now, what should our filter look like? #[AsFilter(formType: TeamFilterType::class, template: '@SyliusBootstrapAdminUi/shared/grid/filter/select.html.twig',)] final class TeamFilter implements FilterInterface ->addFilter(Filter::create('teamName', TeamFilter::class))
  104. Q4: How can you create a Team filter to use

    it on the F1 drivers’ grid? We’ve added the following filter on the grid configuration: Now, what should our filter look like? #[AsFilter(formType: TeamFilterType::class, template: '@SyliusBootstrapAdminUi/shared/grid/filter/select.html.twig',)] final class TeamFilter implements FilterInterface #[AsGridFilter(formType: TeamFilterType::class, template: '@SyliusBootstrapAdminUi/shared/grid/filter/select.html.twig', final class TeamFilter implements FilterInterface ->addFilter(Filter::create('teamName', TeamFilter::class))
  105. Q4: How can you create a Team filter to use

    it on the F1 drivers’ grid? We’ve added the following filter on the grid configuration: Now, what should our filter look like? #[AsFilter(formType: TeamFilterType::class, template: '@SyliusBootstrapAdminUi/shared/grid/filter/select.html.twig',)] final class TeamFilter implements FilterInterface #[AsGridFilter(formType: TeamFilterType::class, template: '@SyliusBootstrapAdminUi/shared/grid/filter/select.html.twig', final class TeamFilter implements FilterInterface #[Filter(formType: TeamFilterType::class, template: '@SyliusBootstrapAdminUi/shared/grid/filter/select.html.twig',)] final class TeamFilter implements FilterInterface ->addFilter(Filter::create('teamName', TeamFilter::class))
  106. Q5: During 1st World War, whose fighter plane used the

    horse 🐴 symbol that became Ferrari’s logo?
  107. Q5: During 1st World War, whose fighter plane used the

    horse 🐴 symbol that became Ferrari’s logo? Alfredo Ferrari
  108. Q5: During 1st World War, whose fighter plane used the

    horse 🐴 symbol that became Ferrari’s logo? Alfredo Ferrari Tazio Nuvolari
  109. Q5: During 1st World War, whose fighter plane used the

    horse 🐴 symbol that became Ferrari’s logo? Alfredo Ferrari Tazio Nuvolari Enzo Ferrari
  110. Q5: During 1st World War, whose fighter plane used the

    horse 🐴 symbol that became Ferrari’s logo? Alfredo Ferrari Tazio Nuvolari Enzo Ferrari Francesco Baracca
  111. Q5: During 1st World War, whose fighter plane used the

    horse 🐴 symbol that became Ferrari’s logo? Alfredo Ferrari Tazio Nuvolari Enzo Ferrari Francesco Baracca
  112. Name Link Sylius Stack (docs) https://stack.sylius.com/ Sylius Stack (Github) https://github.com/Sylius/Stack

    Grid (Github) https://github.com/Sylius/SyliusGridBundle Open F1 API by br-g (Bruno Godefroy) https://openf1.org/ Demo project https://github.com/loic425/openf1