# Sylius Stack Documentation

The Sylius stack is a set of tools for your Symfony projects. It comes with a bunch of components that work great independently, but when they come together, that's when the stack's magic truly operates! Indeed, the highlight of this project is the ability to configure an admin panel UI within minutes.

<div data-full-width="false"><figure><img src="/files/7EIGtmN2uXdyruXEhLit" alt="Overview of an admin dashboard"><figcaption></figcaption></figure></div>

## ⚙️ Installation

* [Getting started](/getting-started)

## 📖 Cookbook

* [How to customize your admin panel](/cookbook/admin_panel)
* [How to use in a DDD architecture](/cookbook/ddd_architecture)

## 🧩 Components

* [**ResourceBundle:** Resource management system, routing and CRUD operations](/resource/index)
* [**GridBundle:** Amazing grids with support of filters and custom fields integrated into Symfony](/grid/index)
* [**AdminUi:** Minimalist generic templates for your admin panels](/admin-ui/getting-started)
* [**BootstrapAdminUi:** Build your Bootstrap admin panels with Sylius and Symfony UX](/bootstrap-admin-ui/getting-started)
* [**TwigExtra:** Additional Twig extensions for your Symfony projects](/twig-extra/getting-started)
* [**TwigHooks:** Composable Twig layouts](/twig-hooks/getting-started)
* [**UiTranslations:** Basic UI translations](https://github.com/Sylius/Stack/blob/main/docs/ui-translations/getting-started.md)


# Getting started

## Setup an admin panel

The Sylius Stack comes with a bunch of components that work great independently, but when they come together, that's when the stack's magic truly operates! Indeed, the highlight of this project is the ability to configure an admin panel UI within minutes.

### Create a new project

You can set up the Sylius Stack on existing Symfony projects, but in the case you are starting from scratch, here is what you need to do.

```bash
# With Composer:
composer create-project symfony/skeleton:"8.0.*" my_project_directory
cd my_project_directory
composer require webapp

# Or with Symfony CLI:
symfony new my_project_directory --version="8.0.*" --webapp
cd my_project_directory
```

### Allow Contrib Recipes

Sylius Stack recipes are hosted in the `symfony/recipes-contrib` repository. To ensure Symfony Flex installs them automatically, enable contrib recipes before requiring the packages:

```bash
composer config extra.symfony.allow-contrib true
```

### Install the package using Composer and Symfony Flex

Go to your project directory and run the following command:

```bash
composer require -W \
  doctrine/orm \
  doctrine/doctrine-bundle \
  pagerfanta/doctrine-orm-adapter \
  symfony/asset-mapper \
  sylius/bootstrap-admin-ui \
  sylius/ui-translations
```

<div data-full-width="false"><figure><img src="/files/blSeJqeGtxQPJh7WVWi2" alt="Flex recipes"><figcaption></figcaption></figure></div>

Type "a" or "p" to configure the packages via Symfony Flex.

Do not forget to set your application secret environment variable:

{% code title=".env" %}

```dotenv
# ...
APP_SECRET=UseYourOwnSecretPlease
```

{% endcode %}

### Run your web server

```bash
docker compose up -d
symfony serve -d
```

The admin panel is ready to use. Now, it's your turn!

<div data-full-width="false"><figure><img src="/files/pWQnXmrkKBry4ZvY4Slv" alt="Admin dashboard overview"><figcaption></figcaption></figure></div>

### Using AssetMapper

To prevent duplicate Ajax calls, disable the auto-initialized Stimulus app and Symfony UX stylesheets from the `sylius/bootstrap-admin-ui` package, so you can take control of Stimulus initialization in your own code.

#### Disabling Stimulus app & Symfony UX stylesheets from third party package

First, you need to disable the Stimulus App started by the `sylius/bootstrap-admin-ui` package and add a custom javascript app hook for the asset mapper.

{% tabs %}
{% tab title="YAML" %}
{% code lineNumbers="true" %}

```yaml
# config/packages/sylius_bootstrap_admin_ui.yaml
# ...
sylius_twig_hooks:
    hooks:
        # ...
        # Disabling Symfony UX stylesheets
        'sylius_admin.base#stylesheets':
            symfony_ux:
                enabled: false    
           
        'sylius_admin.base#javascripts':
            app:
                priority: 200
                template: 'base/javascripts/app.html.twig'
            # Disabling Stimulus App
            symfony_ux:
                enabled: false
```

{% endcode %}
{% endtab %}

{% tab title="PHP" %}
{% code title="config/packages/sylius\_bootstrap\_admin\_ui.php" lineNumbers="true" %}

```php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    // ...

    $containerConfigurator->extension('sylius_twig_hooks', [
        'hooks' => [
            'sylius_admin.base#stylesheets' => [
                // Disabling Symfony UX stylesheets
                'symfony_ux' => [
                    'enabled' => false,
                ],
            ],
            
            'sylius_admin.base#javascripts' => [
                // New hook
                'app' => [
                    'priority' => 200,
                    'template' => 'base/javascripts/app.html.twig',
                ],                
                // Disabling Stimulus App        
                'symfony_ux' => [
                    'enabled' => false,
                ],
            ],
        ],    
    ]);
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% code title="base/javascripts/app.html.twig" lineNumbers="true" %}

```twig
{{ importmap('app') }}
```

{% endcode %}

#### Starting Stimulus App

```js
// assets/bootstrap.js
import { startStimulusApp } from '@symfony/stimulus-bundle';

const app = startStimulusApp();
// register any custom, 3rd party controllers here
// app.register('some_controller_name', SomeImportedController);
```

```js
// assets/app.js
import './bootstrap.js';
// ...
```


# How to customize your admin panel

* [Basic operations](/cookbook/admin_panel/basic_operations)
* [Customizing your grids](/cookbook/admin_panel/grids)
* [Customizing the logo](/cookbook/admin_panel/logo)
* [Customizing the menu](/cookbook/admin_panel/menu)
* [Configuring the security access](/cookbook/admin_panel/security)
* [Customizing the page titles](/cookbook/admin_panel/page_titles)
* [Customizing the metatags](/cookbook/admin_panel/metatags)
* [Using autocompletes](/cookbook/admin_panel/using-autocompletes)
* [Exporting grid data](/cookbook/admin_panel/grid_export)


# Basic operations

In this cookbook, we assume that you have already created a `Book` resource.

{% hint style="info" %}
Learn more on how to [create a Sylius resource](/resource/index/create_new_resource).
{% endhint %}

## List of resources

<div data-full-width="false"><figure><img src="/files/bXPbpz9CqQxiWoW9e8G9" alt="List of books"><figcaption></figcaption></figure></div>

Create a grid for your resource using Symfony's Maker Bundle.

**Note:** To ease the setup, it is recommended to have an existing Doctrine Entity configured.

```shell
bin/console make:grid
bin/console cache:clear # To refresh grid's cache
```

Magic! Here is the generated grid.

```php
final class BookGrid extends AbstractGrid implements ResourceAwareGridInterface
{
    public function __construct()
    {
        // TODO inject services if required
    }

    public static function getName(): string
    {
        return 'app_book';
    }

    public function buildGrid(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            // see https://github.com/Sylius/SyliusGridBundle/blob/master/docs/field_types.md
            ->withFields(
                StringField::create('title')
                    ->setLabel('Title')
                    ->setSortable(true),
                StringField::create('author')
                    ->setLabel('Author')
                    ->setSortable(true),
            )
            ->withMainActions(
                CreateAction::create(),
            )
            ->withItemActions(
                // ShowAction::create(),
                UpdateAction::create(),
                DeleteAction::create(),
            )
            ->withBulkActions(
                DeleteAction::create(),
            )
        ;
    }

    public function getResourceClass(): string
    {
        return Book::class;
    }
```

Configure the `index` operation in your resource.

```php
namespace App\Entity;

use App\Grid\BookGrid;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Index;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    section: 'admin', // This will influence the route name
    routePrefix: '/admin',
    templatesDir: '@SyliusAdminUi/crud', // This directory contains the generic template for your list
    operations: [
        new Index( // This operation will add "index" operation for the books list
            grid: BookGrid::class, // Use the grid class you have generated in previous step
        ), 
    ],    
)]
class Book implements ResourceInterface
{
    //...
}
```

{% hint style="info" %}
Note: When you are in a Sylius project, the `templatesDir` path is: `@SyliusAdmin/shared/crud`
{% endhint %}

Use the Symfony `debug:router` command to check the results.

```shell
bin/console debug:router
```

Your route should look like this:

```shell
 ------------------------------ ---------------------------
  Name                           Path                                           
 ------------------------------ ---------------------------                  
  app_admin_book_index           /admin/books               
```

## Resource creation page

<div data-full-width="false"><figure><img src="/files/5us4BSuZnVuFAPt6g6Gz" alt="Book creation page"><figcaption></figcaption></figure></div>

Create a form type for your resource.

```shell
bin/console make:form
```

Configure the `create` operation in your resource.

```php
namespace App\Entity;

use App\Form\BookType;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    section: 'admin', // This will influence the route name
    routePrefix: '/admin',
    templatesDir: '@SyliusAdminUi/crud', // This directory contains the generic templates
    formType: BookType::class, // The form type you have generated in previous step
    operations: [
        // ...
        new Create(), // This operation will add "create" operation for the book resource
    ],    
)]
class Book implements ResourceInterface
{
    //...
}
```

{% hint style="info" %}
Note: When you are in a Sylius project, the `templatesDir` path is: `@SyliusAdmin/shared/crud`
{% endhint %}

Use the Symfony `debug:router` command to check the results.

```shell
bin/console debug:router
```

Your route should look like this:

```shell
 ------------------------------ ---------------------------
  Name                           Path                                           
 ------------------------------ ---------------------------                  
  app_admin_book_create           /admin/books/new               
```

## Resource edition page

<div data-full-width="false"><figure><img src="/files/kCODQgOncwyAFCVh6jCk" alt="Book edition page"><figcaption></figcaption></figure></div>

Ensure you already created the Symfony form type in the [previous section](#resource-creation-page).

Configure the `update` operation in your resource.

```php
namespace App\Entity;

use App\Form\BookType;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Update;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    section: 'admin', // This will influence the route name
    routePrefix: '/admin',
    templatesDir: '@SyliusAdminUi/crud', // This directory contains the generic templates
    formType: BookType::class, // The form type you have generated in previous chapter
    operations: [
        // ...
        new Update(), // This operation will add "update" operation for the book resource
    ],    
)]
class Book implements ResourceInterface
{
    //...
}
```

{% hint style="info" %}
Note: When you are in a Sylius project, the `templatesDir` path is: `@SyliusAdmin/shared/crud`
{% endhint %}

Use the Symfony `debug:router` command to check the results.

```shell
bin/console debug:router
```

Your route should look like this:

```shell
 ------------------------------ ---------------------------
  Name                           Path                                           
 ------------------------------ ---------------------------                  
  app_admin_book_update           /admin/books/{id}/edit              
```

## Resource details page

<div data-full-width="false"><figure><img src="/files/gQTdUYW1TXW36fnEfNZF" alt="Book details page"><figcaption></figcaption></figure></div>

Configure the `show` operation in your resource.

```php
namespace App\Entity;

use App\Form\BookType;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Show;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    section: 'admin', // This will influence the route name
    routePrefix: '/admin',
    templatesDir: '@SyliusAdminUi/crud', // This directory contains the generic templates
    operations: [
        // ...
        new Show(), // This operation will add "show" operation for the book resource
    ],    
)]
class Book implements ResourceInterface
{
    //...
}
```

{% hint style="info" %}
Note: When you are in a Sylius project, the `templatesDir` path is: `@SyliusAdmin/shared/crud`
{% endhint %}

Use the Symfony `debug:router` command to check the results.

```shell
bin/console debug:router
```

Your route should look like this:

```shell
 ------------------------------ ---------------------------
  Name                           Path                                           
 ------------------------------ ---------------------------                  
  app_admin_book_show           /admin/books/{id}              
```

Now we need to configure the templates.

{% tabs %}
{% tab title="YAML" %}
{% code lineNumbers="true" %}

```yaml
# config/packages/sylius_bootstrap_admin_ui.yaml
# ...
sylius_twig_hooks:
    hooks:
        # ...
        # This will create the body block
        'sylius_admin.book.show.content':
            body:
                template: 'book/show/content/body.html.twig'

```

{% endcode %}
{% endtab %}

{% tab title="PHP" %}
{% code lineNumbers="true" %}

```php
// config/packages/sylius_bootstrap_admin_ui.php
declare(strict_types=1);

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return static function (ContainerConfigurator $container): void {
    $container->extension('sylius_twig_hooks', [
        'hooks' => [
            // ...
            // This will create the body block
            'sylius_admin.book.show.content' => [
                'body' => [
                    'template' => 'book/show/content/body.html.twig',
                ],
            ],
        ],
    ]);
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% code title="templates/book/show/content/body.html.twig" overflow="wrap" lineNumbers="true" %}

```twig
{% set book = hookable_metadata.context.book %}

<div class="page-body">
    <div class="container-xl">
        <div class="row">
            <div class="col-12">
                <p>
                    {{ book.description|nl2br }}
                </p>
            </div>
        </div>
    </div>
</div>
```

{% endcode %}

{% hint style="info" %}
Note that you can also [replace the default title](/cookbook/admin_panel/page_titles).
{% endhint %}


# Customizing your grids

<div data-full-width="false"><figure><img src="/files/LgYo6f7m6JFOIaJPS8jH" alt="Overview of an admin dashboard"><figcaption></figcaption></figure></div>

Based on the grid generated by default, our goal here is to obtain a nicely customized grid with autocomplete filters and more!

<figure><img src="/files/IxEll0tkvc6JsXRAKSS1" alt="Overview of an admin dashboard"><figcaption></figcaption></figure>

Let's imagine we have the following grid.

{% code title="src/Grid/TalkGrid.php" lineNumbers="true" %}

```php
<?php

namespace App\Grid;

use App\Entity\Talk;
use Sylius\Bundle\GridBundle\Builder\Action\CreateAction;
use Sylius\Bundle\GridBundle\Builder\Action\DeleteAction;
use Sylius\Bundle\GridBundle\Builder\Action\ShowAction;
use Sylius\Bundle\GridBundle\Builder\Action\UpdateAction;
use Sylius\Bundle\GridBundle\Builder\Field\DateTimeField;
use Sylius\Bundle\GridBundle\Builder\Field\StringField;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    name: 'app_talk',
    resourceClass: Talk::class,
)]
final class TalkGrid extends AbstractGrid
{
    public function __construct()
    {
        // TODO inject services if required
    }

    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            // see https://github.com/Sylius/SyliusGridBundle/blob/master/docs/field_types.md
            ->withFields(
                StringField::create('title')
                    ->setLabel('Title')
                    ->setSortable(true),
                StringField::create('description')
                    ->setLabel('Description')
                    ->setSortable(true),
                DateTimeField::create('startsAt')
                    ->setLabel('StartsAt'),
                DateTimeField::create('endsAt')
                    ->setLabel('EndsAt'),
                StringField::create('track')
                    ->setLabel('Track')
                    ->setPath('track.value')
                    ->setSortable(true),    
            )
            ->withMainActions(
                CreateAction::create(),
            )
            ->withItemActions(
               // ShowAction::create(),
                UpdateAction::create(),
                DeleteAction::create(),
            )
            ->withBulkActions(
                DeleteAction::create(),
            )
        ;
    }
}
```

{% endcode %}

## Fields

<div data-full-width="false"><figure><img src="/files/76KwYEw7vUoRgnrK0dXd" alt="Overview of an admin dashboard"><figcaption></figcaption></figure></div>

Let's clean up our grid and remove unnecessary fields.

{% code title="src/Grid/TalkGrid.php" lineNumbers="true" %}

```php
<?php

namespace App\Grid;

use App\Entity\Talk;
use Sylius\Bundle\GridBundle\Builder\Action\CreateAction;
use Sylius\Bundle\GridBundle\Builder\Action\DeleteAction;
use Sylius\Bundle\GridBundle\Builder\Action\ShowAction;
use Sylius\Bundle\GridBundle\Builder\Action\UpdateAction;
use Sylius\Bundle\GridBundle\Builder\Field\DateTimeField;
use Sylius\Bundle\GridBundle\Builder\Field\StringField;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    name: 'app_talk',
    resourceClass: Talk::class,
)]
final class TalkGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFields(
                StringField::create('title')
                    ->setLabel('Title')
                    ->setSortable(true),
                DateTimeField::create('startsAt')
                    ->setLabel('StartsAt'),
            )
            ->withMainActions(
                CreateAction::create(),
            )
            ->withItemActions()
                // ShowAction::create(),
                UpdateAction::create(),
                DeleteAction::create(),
            )
            ->withBulkActions(
                DeleteAction::create(),
            )
        ;
    }
}
```

{% endcode %}

We have removed the `description`, `endsAt` and `track` grid fields.

<div data-full-width="false"><figure><img src="/files/76KwYEw7vUoRgnrK0dXd" alt="Overview of an admin dashboard"><figcaption></figcaption></figure></div>

## Adding the speaker avatar using Twig field

<div data-full-width="false"><figure><img src="/files/bgvaZ28UmXM7PkREvwxt" alt="Overview of an admin dashboard"><figcaption></figcaption></figure></div>

Now, let's add the speaker avatar into our talk grid.

```php
<?php

namespace App\Grid;

use Sylius\Bundle\GridBundle\Builder\Field\TwigField;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;
// ...

#[AsGrid(
    name: 'app_talk',
    resourceClass: Talk::class,
)]
final class TalkGrid extends AbstractGrid
{
    // ...

    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFields(
                TwigField::create('avatar', 'talk/grid/field/speaker_avatar.html.twig')
                    ->setPath('.')
                    ->setLabel('app.ui.avatar'),
            )
            // ...
        ;
    }

    // ...
}
```

{% code title="templates/talk/grid/speaker\_avatar.html.twig" lineNumbers="true" %}

```php
{{ avatar.default(avatar_path, 'img-thumbnail') }}
```

{% endcode %}

## Filters

### Adding an autocomplete filter

<div data-full-width="false"><figure><img src="/files/hD3VprldZiPN2wwbiY3w" alt="Overview of an admin dashboard"><figcaption></figcaption></figure></div>

We'd like to filter our talks by a specific speaker.

So, let's start by creating a FormType following the [Symfony UX Autocomplete Documentation](https://symfony.com/bundles/ux-autocomplete/current/index.html#usage-in-a-form-with-ajax)

{% code title="src/Form/SpeakerAutocompleteType.php" lineNumbers="true" %}

```php
declare(strict_types=1);

namespace App\Form;

use App\Entity\Speaker;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;

#[AsEntityAutocompleteField(
    alias: 'app_admin_speaker',
    route: 'ux_entity_autocomplete_admin',
)]
final class SpeakerAutocompleteType extends AbstractType
{
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'class' => Speaker::class,
            'choice_label' => 'fullName',
        ]);
    }

    public function getParent(): string
    {
        return BaseEntityAutocompleteType::class;
    }
}
```

{% endcode %}

Now we need to create our custom Grid filter.

{% code title="src/Grid/Filter/SpeakerFilter.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid\Filter;

use App\Form\SpeakerAutocompleteType;
use Sylius\Component\Grid\Attribute\AsFilter;
use Sylius\Component\Grid\Data\DataSourceInterface;
use Sylius\Component\Grid\Filter\EntityFilter;
use Sylius\Component\Grid\Filtering\FilterInterface;

#[AsFilter(
    formType: SpeakerAutocompleteType::class,
    template: '@SyliusBootstrapAdminUi/shared/grid/filter/select.html.twig',
)]
final class SpeakerFilter implements FilterInterface
{
    public function __construct(
        private readonly EntityFilter $entityFilter,
    ) {
    }

    public function apply(DataSourceInterface $dataSource, string $name, mixed $data, array $options): void
    {
        // We simply reuse the logic of the built-in EntityFilter provided by the Sylius Grid package.
        $this->entityFilter->apply($dataSource, $name, $data, $options);
    }
}
```

{% endcode %}

Then, we add our `SpeakerFilter` to our grid.

{% code title="src/Grid/TalkGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Grid\Filter\SpeakerFilter;
use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Bundle\GridBundle\Grid\ResourceAwareGridInterface;
use Sylius\Component\Grid\Attribute\AsGrid;
// ...

#[AsGrid(
    name: 'app_talk',
    resourceClass: Talk::class,
)]
final class TalkGrid extends AbstractGrid
{
    // ...

    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFilters(
                Filter::create(name: 'speaker', type: SpeakerFilter::class)
                    ->setLabel('app.ui.speaker')
                    ->setOptions(['fields' => ['speaker.id']])
            );
            
            // ...
    }

    // ...
}
```

{% endcode %}


# Customizing the logo

## How to customize the sidebar logo

To customize the sidebar logo, you need to set new logo template at `sylius_admin.common.component.sidebar.logo` twig hook. Choose the YAML or the PHP version.

{% tabs %}
{% tab title="YAML" %}
{% code lineNumbers="true" %}

```yaml
# config/packages/sylius_bootstrap_admin_ui.yaml
# ...
sylius_twig_hooks:
    hooks:
        # ...
        'sylius_admin.common.component.sidebar.logo':
            image:
                # template: '@SyliusBootstrapAdminUi/shared/crud/common/sidebar/logo/image.html.twig'
                template: 'shared/crud/common/sidebar/logo/image.html.twig'

```

{% endcode %}
{% endtab %}

{% tab title="PHP" %}
{% code lineNumbers="true" %}

```php
// config/packages/sylius_bootstrap_admin_ui.php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    // ...

    // Add these following lines to define your own Twig template for the logo.
    $containerConfigurator->extension('sylius_twig_hooks', [
        'hooks' => [
            'sylius_admin.common.component.sidebar.logo' => [
                'image' => [
                    // 'template' => '@SyliusBootstrapAdminUi/shared/crud/common/sidebar/logo/image.html.twig',
                    'template' => 'shared/crud/common/sidebar/logo/image.html.twig',
                ],
            ],
        ],
        
    ]);
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

```twig
{# templates/shared/crud/common/sidebar/logo/image.html.twig #}

<img src="{{ asset('images/logo.png') }}" alt="Your Brand name" class="navbar-brand-image" />


  
```

## How to customize the login page logo

To customize the login page logo,you need to set new logo template at `sylius_admin.security.login.logo` twig hook. Choose the YAML or the PHP version.

{% tabs %}
{% tab title="YAML" %}
{% code lineNumbers="true" %}

```yaml
# config/packages/sylius_bootstrap_admin_ui.yaml
# ...
sylius_twig_hooks:
    hooks:
        # ...
        'sylius_admin.security.login.page.logo':
            image:
                # template: '@SyliusBootstrapAdminUi/security/common/logo/image.html.twig'
                template: 'security/common/logo/image.html.twig'
```

{% endcode %}
{% endtab %}

{% tab title="PHP" %}
{% code lineNumbers="true" %}

```php
// config/packages/sylius_bootstrap_admin_ui.php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    // ...

    // Add the following lines to define your own Twig template for the logo.
    $containerConfigurator->extension('sylius_twig_hooks', [
        'hooks' => [
            'sylius_admin.security.login.logo' => [
                'image' => [
                    // 'template' => '@SyliusBootstrapAdminUi/security/common/logo/image.html.twig'
                    'template' => 'security/common/logo/image.html.twig',
                ],
            ],
        ],
    ]);
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

```twig
<img src="{{ asset('images/logo.png') }}" alt="Your Brand name" class="sylius navbar-brand-image">
```


# Customizing the menu

## How to customize the sidebar menu

### Decorate the sidebar menu

<div data-full-width="false"><figure><img src="/files/j8t8hNLrPxwzONfim4Y8" alt="Sidebar menu"><figcaption></figcaption></figure></div>

To customize the admin menu, you need to decorate the `sylius_admin_ui.knp.menu_builder` service.

```php
declare(strict_types=1);

namespace App\Menu;

use Knp\Menu\FactoryInterface;
use Knp\Menu\ItemInterface;
use Sylius\AdminUi\Knp\Menu\MenuBuilderInterface;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;

#[AsDecorator(decorates: 'sylius_admin_ui.knp.menu_builder')]
final readonly class MenuBuilder implements MenuBuilderInterface
{
    public function __construct(
        private readonly MenuBuilderInterface $menuBuilder,
    ) {
    }

    public function createMenu(array $options): ItemInterface
    {
        $menu = $this->menuBuilder->createMenu($options);

        $menu
            ->addChild('dashboard', [
                'route' => 'sylius_admin_ui_dashboard',
            ])
            ->setLabel('sylius.ui.dashboard')
            ->setLabelAttribute('icon', 'tabler:dashboard')
        ;

        return $menu;
    }
}
```

### Add submenu items

<div data-full-width="false"><figure><img src="/files/50YRLw70rC1BmxvaqXP2" alt="Submenu items"><figcaption></figcaption></figure></div>

Now you can add submenu items:

```php
// ...
#[AsDecorator(decorates: 'sylius_admin_ui.knp.menu_builder')]
final readonly class MenuBuilder implements MenuBuilderInterface
{
    // ...
    
    public function createMenu(array $options): ItemInterface
    {
        $menu = $this->menuBuilder->createMenu($options);
        // ...
        $this->addLibrarySubMenu($menu);

        return $menu;
    }
    
    private function addLibrarySubMenu(ItemInterface $menu): void
    {
        $library = $menu
            ->addChild('library')
            ->setLabel('app.ui.library')
            ->setLabelAttribute('icon', 'tabler:books')
        ;

        $library->addChild('books', ['route' => 'app_admin_book_index'])
            ->setLabel('app.ui.books')
            ->setLabelAttribute('icon', 'book')
        ;
    }
}
```

{% hint style="success" %}
**🧠 Collapse your custom menu by default**

It's possible to expand your parent menu category on page load by default. For that, you have to set the `setExtra` attribute like this:

```php
$library = $menu
    ->addChild('library')
    ->setLabel('app.ui.library')
    ->setLabelAttribute('icon', 'tabler:books')
    ->setExtra('always_open', true);
```

However, ensure that you set the attribute in the parent menu, not in one of the child menu items.
{% endhint %}


# Configuring the security access

<div data-full-width="false"><figure><img src="/files/OhbaFdhTDQFnvuRkdwud" alt="Login page"><figcaption></figcaption></figure></div>

Now that you have an admin panel, you want to make sure admin users are the only ones allowed to access its URL. To secure your back-office interface, you can simply resort to Symfony's Security configuration with 4 basic steps :

* [Create a User](#create-a-user-entity)
* [Create the user provider](#configure-the-user-provider)
* [Configure firewalls](#configure-the-firewall)
* [Configure the access control authorization](#configure-access-control-authorization)

## Create a user entity

You can use the Symfony maker to create a new user.

```shell
bin/console make:user
```

{% hint style="info" %}
Learn more on how to [create a User](https://symfony.com/doc/current/security.html#the-user)
{% endhint %}

## Configure the user provider

Here is an example of a user provider configuration:

{% code title="config/packages/security.yaml" lineNumbers="true" %}

```yaml
security:
    # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
    # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
    providers:
        # used to reload user from session & other features (e.g. switch_user)
        app_admin_user_provider:
            entity:
                class: App\Entity\User
                property: email
```

{% endcode %}

{% hint style="info" %}
Learn more on how to [create a user provider on the Symfony documentation](https://symfony.com/doc/current/security.html#the-user)
{% endhint %}

## Configure the firewall

Here is an example of how to configure a firewall for your admin routes:

{% code title="config/packages/security.yaml" %}

```yaml
security:
    firewalls:
        # ...
        admin:
            context: admin
            pattern: '/admin(?:/.*)?$'
            provider: app_admin_user_provider # Reuse the provider key you configured on providers section
            form_login:
                # These routes are provided by Sylius Admin Ui package
                login_path: sylius_admin_ui_login 
                check_path: sylius_admin_ui_login_check
                default_target_path: sylius_admin_ui_dashboard
            logout:
                # These routes are provided by Sylius Admin Ui package
                path: sylius_admin_ui_logout
                target: sylius_admin_ui_login
        main:
            lazy: true
```

{% endcode %}

{% hint style="warning" %}
It's important to move the main block under the admin configuration. Otherwise the admin login functionality won't work properly.
{% endhint %}

{% hint style="info" %}
Learn more on how to [configure the firewall on the Symfony documentation](https://symfony.com/doc/current/security.html#the-firewall)
{% endhint %}

## Configure Access Control Authorization

Only admin users will have access to "/admin" routes.

{% code title="config/packages/security.yaml" %}

```yaml
security:
    access_control:
        - { path: ^/admin/login, roles: PUBLIC_ACCESS }
        - { path: ^/admin/logout, roles: PUBLIC_ACCESS }
        - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/, roles: PUBLIC_ACCESS }
```

{% endcode %}

{% hint style="info" %}
Learn more on how to [configure Access Control Authorization on the Symfony documentation](https://symfony.com/doc/current/security.html#access-control-authorization)
{% endhint %}


# Customizing the page titles

## Changing the default title for a specific page

<div data-full-width="false"><figure><img src="/files/XIjUQygwmunrLjUFBHat" alt="Title changed to Browsing speakers"><figcaption></figcaption></figure></div>

By default, each page has a default title based on both page location and resource name. If you're not happy with the preset title for a specific page and would like to customize it, you can easily change it using Twig Hooks.

Search for "title\_block" in the call graph of the Symfony debug profiler in the `Twig Hooks` section.

<div data-full-width="false"><figure><img src="/files/VaZYU39TUPlgB94dQisQ" alt="Title block in profiler"><figcaption></figcaption></figure></div>

We're going to reuse this hook and its template in our config file and add a `header` key:

{% tabs %}
{% tab title="YAML" %}
{% code title="config/packages/sylius\_bootstrap\_admin\_ui.yaml" lineNumbers="true" %}

```yaml
# ...
sylius_twig_hooks:
    hooks:
        # ...
        'sylius_admin.speaker.index.content.header.title_block': # The speaker index title block
            title:
                template: '@SyliusBootstrapAdminUi/shared/crud/common/content/header/title_block/title.html.twig'
                configuration:
                    title: app.ui.browsing_speakers # here is our title override
```

{% endcode %}
{% endtab %}

{% tab title="PHP" %}
{% code title="config/packages/sylius\_bootstrap\_admin\_ui.php" lineNumbers="true" %}

```php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    // ...
    $containerConfigurator->extension('sylius_twig_hooks', [
        'hooks' => [
            // The speaker index title block
            'sylius_admin.speaker.index.content.header.title_block' => [
                'title' => [
                    'template' => '@SyliusBootstrapAdminUi/shared/crud/common/content/header/title_block/title.html.twig',
                    'configuration' => [
                        'title' => 'app.ui.browsing_speakers' // here is our title override 
                    ],
                ],
            ],
        ],        
    ]);
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

<div data-full-width="false"><figure><img src="/files/RiwuSlKr8yx9iICUv88f" alt="Show book title"><figcaption></figcaption></figure></div>

Note that you can also use [Symfony Expression Language](https://symfony.com/doc/current/components/expression_language.html) in the configuration key for dynamic titles:

{% tabs %}
{% tab title="YAML" %}
{% code title="config/packages/sylius\_bootstrap\_admin\_ui.yaml" lineNumbers="true" %}

```yaml
# ...
sylius_twig_hooks:
    hooks:
        # ...
        'sylius_admin.book.show.content.header.title_block': # The show page title block
            title:
                template: '@SyliusBootstrapAdminUi/shared/crud/common/content/header/title_block/title.html.twig'
                configuration:
                    title: '@=_context.book.getTitle()' # Use the current book title
```

{% endcode %}
{% endtab %}

{% tab title="PHP" %}
{% code title="config/packages/sylius\_bootstrap\_admin\_ui.php" lineNumbers="true" %}

```php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    // ...
    $containerConfigurator->extension('sylius_twig_hooks', [
        'hooks' => [
            // The show page title block
            'sylius_admin.book.show.content.header.title_block' => [
                'title' => [
                    'template' => '@SyliusBootstrapAdminUi/shared/crud/common/content/header/title_block/title.html.twig',
                    'configuration' => [
                        'title' => '@=_context.book.getTitle()' // Use the current book title
                    ],
                ],
            ],
        ],        
    ]);
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

`@=_context` contains all the current Twig vars.

## Adding an icon

<div data-full-width="false"><figure><img src="/files/ikOKqVq2koxO6HyZbSHz" alt="Title with icon"><figcaption></figcaption></figure></div>

To add an icon to the page title, you need to use Twig hooks configuration.

Search for "title\_block" in the call graph of the Symfony debug profiler in the `Twig Hooks` section. We're going to reuse this hook and its template in our config file.

<div data-full-width="false"><figure><img src="/files/VaZYU39TUPlgB94dQisQ" alt="Title block in profiler"><figcaption></figcaption></figure></div>

Here's an example to define a "users" icon on a speaker list.

{% tabs %}
{% tab title="YAML" %}
{% code title="config/packages/sylius\_bootstrap\_admin\_ui.yaml" lineNumbers="true" %}

```yaml
# ...
sylius_twig_hooks:
    hooks:
        # ...
        'sylius_admin.speaker.index.content.header.title_block':
            title:
                # We need to reuse the same template as 'sylius_admin.common.index.content.header.title_block'
                template: '@SyliusBootstrapAdminUi/shared/crud/common/content/header/title_block/title.html.twig'
                configuration:
                    icon: tabler:users # You can use any icon from Symfony UX icons.
```

{% endcode %}
{% endtab %}

{% tab title="PHP" %}
{% code title="config/packages/sylius\_bootstrap\_admin\_ui.php" lineNumbers="true" %}

```php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    // ...
    $containerConfigurator->extension('sylius_twig_hooks', [
        'hooks' => [
            'sylius_admin.speaker.index.content.header.title_block' => [
                'title' => [
                    // # We need to reuse the same template as 'sylius_admin.common.index.content.header.title_block'
                    'template' => '@SyliusBootstrapAdminUi/shared/crud/common/content/header/title_block/title.html.twig',
                    'configuration' => [
                        'icon' => 'tabler:users' // You can use any icon from Symfony UX icons.
                    ],
                ],
            ],
        ],        
    ]);
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

You can also define a default icon for every "index" pages.

<div data-full-width="false"><figure><img src="/files/LUMpFYdjtZoAyoPCRdef" alt="Icon for index pages"><figcaption></figcaption></figure></div>

{% tabs %}
{% tab title="YAML" %}
{% code title="config/packages/sylius\_bootstrap\_admin\_ui.yaml" lineNumbers="true" %}

```yaml
# ...
sylius_twig_hooks:
    hooks:
        # ...
        'sylius_admin.common.index.content.header.title_block':
            title:
                configuration:
                    icon: tabler:list-details
```

{% endcode %}
{% endtab %}

{% tab title="PHP" %}
{% code title="config/packages/sylius\_bootstrap\_admin\_ui.php" lineNumbers="true" %}

```php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    // ...
    $containerConfigurator->extension('sylius_twig_hooks', [
        'hooks' => [
            'sylius_admin.common.index.content.header.title_block' => [
                'title' => [
                    'configuration' => [
                        'icon' => 'list-details'
                    ],
                ],
            ],
        ],        
    ]);
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Adding a subheader

<div data-full-width="false"><figure><img src="/files/dnTFbbOFsIe6ZKtpCy7v" alt="Title with subheader"><figcaption></figcaption></figure></div>

To add a subheader to the page title, you need to use Twig hooks configuration.

See the [previous section](#adding-an-icon) to see how to search for the title block.

Here's an example to define a subheader on a speaker list.

{% tabs %}
{% tab title="YAML" %}
{% code title="config/packages/sylius\_bootstrap\_admin\_ui.yaml" lineNumbers="true" %}

```yaml
# ...
sylius_twig_hooks:
    hooks:
        # ...
        'sylius_admin.speaker.index.content.header.title_block':
            title:
                # We need to reuse the same template as 'sylius_admin.common.index.content.header.title_block'
                template: '@SyliusBootstrapAdminUi/shared/crud/common/content/header/title_block/title.html.twig'
                configuration:
                    subheader: app.ui.managing_your_speakers # You also need to add this key to your translations.
```

{% endcode %}
{% endtab %}

{% tab title="PHP" %}
{% code title="config/packages/sylius\_bootstrap\_admin\_ui.php" lineNumbers="true" %}

```php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    // ...
    $containerConfigurator->extension('sylius_twig_hooks', [
        'hooks' => [
            'sylius_admin.speaker.index.content.header.title_block' => [
                'title' => [
                    // We need to reuse the same template as 'sylius_admin.common.index.content.header.title_block'
                    'template' => '@SyliusBootstrapAdminUi/shared/crud/common/content/header/title_block/title.html.twig',
                    'configuration' => [
                        'subheader' => 'app.ui.managing_your_speakers' // You also need to add this key to your translations.
                    ],
                ],
            ],
        ],        
    ]);
};
```

{% endcode %}
{% endtab %}
{% endtabs %}


# Customizing the metatags

## Adding metatags

To add new `<head>` meta tags, you can use the `sylius_admin#metatags` hook. This is useful for adding a favicon or SEO meta tags, for example. You can register your own Twig template for meta tags via YAML or PHP.

{% tabs %}
{% tab title="YAML" %}
{% code title="config/packages/sylius\_bootstrap\_admin\_ui.yaml" lineNumbers="true" %}

```yaml
# ...
sylius_twig_hooks:
    hooks:
        # ...
        'sylius_admin.base#metatags':
            favicon:
                template: 'favicon.html.twig'
            seo_metatags:
                template: 'seo_metatags.html.twig'
```

{% endcode %}
{% endtab %}

{% tab title="PHP" %}
{% code title="config/packages/sylius\_bootstrap\_admin\_ui.php" lineNumbers="true" %}

```php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    // ...

    // Define your own Twig template for the favicon.
    $containerConfigurator->extension('sylius_twig_hooks', [
        'hooks' => [
            'sylius_admin.base#metatags' => [
                'favicon' => [
                    'template' => 'favicon.html.twig',
                ],
                'seo_metatags' => [
                    'template' => 'seo_metatags.html.twig',
                ],
            ],
        ],

    ]);
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% code title="templates/favicon.html.twig" %}

```twig
<link rel="icon" type="image/x-icon" href="{{ asset('images/your_favicon.svg') }}" />
```

{% endcode %}

{% code title="templates/seo\_metatags.html.twig" %}

```twig
<meta name="description" content="Your custom description">
```

{% endcode %}


# Using autocompletes

The [SyliusBootstrapAdminUi](/bootstrap-admin-ui/getting-started) package uses[ Symfony UX ](https://ux.symfony.com/)under the hood. Thus, [UX autocomplete](https://symfony.com/bundles/ux-autocomplete/current/index.html) is already setup and configured in your admin panel. This means that any simple `ChoiceType` filter form can be turned into an autocomplete simply by setting `autocomplete` to `true` in form options.

```php
public function configureOptions(OptionsResolver $resolver): void
{
        $resolver->setDefaults([
            'choices' => $this->getChoices(),
            'placeholder' => 'sylius.ui.all',
            'autocomplete' => true,
        ]);
}

public function getParent(): string
{
    return ChoiceType::class;
}

```

However, if your autocomplete filter requires fetching data from another entity, you will need to use a `BaseEntityAutocompleteType` in order to fetch your options via AJAX.

All you need to start leveraging this functionality is a bit of routing config.

### Configure the entity autocomplete route

{% tabs %}
{% tab title="PHP" %}
{% code title="config/routes/ux\_autocomplete.php" %}

```php
<?php

declare(strict_types=1);

use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;

return static function (RoutingConfigurator $routingConfigurator): void {
    // ...
    $routingConfigurator
        ->add('ux_entity_autocomplete_admin', '/admin/autocomplete/{alias}')
        ->controller('ux.autocomplete.entity_autocomplete_controller')
    ;
};

```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/routes/ux\_autocomplete.yaml" %}

```yaml
# ...
ux_entity_autocomplete_admin:
    path: '/admin/autocomplete/{alias}'
    controller: 'ux.autocomplete.entity_autocomplete_controller'
```

{% endcode %}
{% endtab %}
{% endtabs %}

This adds a new `ux_entity_autocomplete_admin` AJAX route dedicated to your autocompletes.

### Add a grid filter with entity autocomplete

First, you need to create an [entity autocomplete field ](https://symfony.com/bundles/ux-autocomplete/current/index.html#usage-in-a-form-with-ajax).

{% code title="src/Form/SpeakerAutocompleteType.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Form;

use App\Entity\Speaker;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;

#[AsEntityAutocompleteField(
    alias: 'app_admin_speaker',
    route: 'ux_entity_autocomplete_admin', // Use the route you just configured.
)]
final class SpeakerAutocompleteType extends AbstractType
{
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'class' => Speaker::class,
            'choice_label' => 'fullName',
        ]);
    }

    public function getParent(): string
    {
        return BaseEntityAutocompleteType::class;
    }
}
```

{% endcode %}

Then, you need to create your custom grid filter.

{% tabs %}
{% tab title="AsFilter attribute - SyliusGridBundle v1.14+" %}
{% code title="src/Grid/Filter/SpeakerFilter.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid\Filter;

use App\Form\SpeakerAutocompleteType;
use Sylius\Component\Grid\Attribute\AsFilter;
use Sylius\Component\Grid\Data\DataSourceInterface;
use Sylius\Component\Grid\Filter\EntityFilter;
use Sylius\Component\Grid\Filtering\FilterInterface;

#[AsFilter(
    formType: SpeakerAutocompleteType::class,
    template: '@SyliusBootstrapAdminUi/shared/grid/filter/select.html.twig', // optional
)]
final class SpeakerFilter implements FilterInterface
{
    public function __construct(
        private readonly EntityFilter $entityFilter,
    ) {
    }

    public function apply(DataSourceInterface $dataSource, string $name, mixed $data, array $options): void
    {
        $this->entityFilter->apply($dataSource, $name, $data, $options);
    }
  }
```

{% endcode %}
{% endtab %}

{% tab title="SyliusGridBundle v1.13 (legacy)" %}
First, create your custom filter class.

{% code title="src/Grid/Filter/SpeakerFilter.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid\Filter;

use App\Form\SpeakerAutocompleteType;
use Sylius\Component\Grid\Data\DataSourceInterface;
use Sylius\Component\Grid\Filter\EntityFilter;
use Sylius\Component\Grid\Filtering\ConfigurableFilterInterface;

final class SpeakerFilter implements ConfigurableFilterInterface
{
    public function __construct(
        private readonly EntityFilter $entityFilter,
    ) {
    }

    public function apply(DataSourceInterface $dataSource, string $name, mixed $data, array $options): void
    {
        $this->entityFilter->apply($dataSource, $name, $data, $options);
    }

    public static function getFormType(): string
    {
        return SpeakerAutocompleteType::class;
    }

    public static function getType(): string
    {
        return self::class; // this will allow us to use FQCN instead of a string key.
    }
  
  }
```

{% endcode %}

Then, configure the Twig template this filter will use.

If you use PHP config files (which we recommend you do!), here is how you configure a filter template:

{% code title="config/packages/sylius\_grid.php" %}

```php
<?php

declare(strict_types=1);

use App\Grid\Filter\SpeakerFilter;
use App\Grid\Template\FilterTemplate;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    $containerConfigurator->extension('sylius_grid', [
        'templates' => [
            'filter' => [
               SpeakerFilter::class => '@SyliusBootstrapAdminUi/shared/grid/filter/select.html.twig',
            ],
        ],
    ]);
};
```

{% endcode %}

Otherwise, if you're using YAML config files, here is the config for the filter template:

{% code title="config/packages/sylius\_grid.yaml" %}

```yaml
sylius_grid:
    # ...
    templates:
        filter:
            'App\Grid\Filter\SpeakerFilter': '@SyliusBootstrapAdminUi/shared/grid/filter/entity.html.twig'
```

{% endcode %}
{% endtab %}
{% endtabs %}

Now that the filter is configured, you can use it inside any grid.

{% code title="src/Grid/TalkGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Grid\Filter\SpeakerFilter;
use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Bundle\GridBundle\Grid\ResourceAwareGridInterface;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(resourceClass: Talk::class)]
final class TalkGrid extends AbstractGrid
{
    // ...

    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            // ...
            ->withFilters(
                Filter::create(name: 'speaker', type: SpeakerFilter::class)
                    ->setLabel('app.ui.speaker')
                    ->setOptions(['fields' => ['speakers.id']]),
            )
            // ...
        ;
    }
    
    // ...
}
```

{% endcode %}


# Exporting grid data

In this cookbook, we assume that you have already created a `Book` resource and configured a grid to show a book list.

In this example, we'll create a CSV export.

<figure><img src="/files/atkGdOdWvQnyA2u8Vz4z" alt="Exporting grid data"><figcaption></figcaption></figure>

## The responder

First, create the responder using the [https://github.com/thephpleague/csv](https://github.com/Sylius/Stack/tree/main/docs/cookbook/admin_panel/league/csv/README.md) package.

{% code title="src/Responder/ExportGridToCsvResponder.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Responder;

use League\Csv\Writer;
use Pagerfanta\PagerfantaInterface;
use Sylius\Component\Grid\Definition\Field;
use Sylius\Component\Grid\Renderer\GridRendererInterface;
use Sylius\Component\Grid\View\GridViewInterface;
use Sylius\Resource\Context\Context;
use Sylius\Resource\Metadata\Operation;
use Sylius\Resource\State\ResponderInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Contracts\Translation\TranslatorInterface;
use Webmozart\Assert\Assert;

final readonly class ExportGridToCsvResponder implements ResponderInterface
{
    public function __construct(
        #[Autowire(service: 'sylius.grid.renderer')]
        private GridRendererInterface $gridRenderer,
        private TranslatorInterface $translator,
    ) {
    }

    /**
     * @param GridViewInterface $data
     */
    public function respond(mixed $data, Operation $operation, Context $context): mixed
    {
        Assert::isInstanceOf($data, GridViewInterface::class);

        $response = new StreamedResponse(function () use ($data) {
            $output = fopen('php://output', 'w');

            if (false === $output) {
                throw new \RuntimeException('Unable to open output stream.');
            }

            $writer = Writer::from($output);

            $fields = $this->sortFields($data->getDefinition()->getFields());
            $this->writeHeaders($writer, $fields);
            $this->writeRows($writer, $fields, $data);
        });

        $response->headers->set('Content-Type', 'text/csv; charset=UTF-8');
        $response->headers->set('Content-Disposition', 'attachment; filename="export.csv"');

        return $response;
    }

    /**
     * @param Field[] $fields
     */
    private function writeHeaders(Writer $writer, array $fields): void
    {
        $labels = array_map(fn (Field $field) => $this->translator->trans($field->getLabel()), $fields);

        $writer->insertOne($labels);
    }

    /**
     * @param Field[] $fields
     */
    private function writeRows(Writer $writer, array $fields, GridViewInterface $gridView): void
    {
        /** @var PagerfantaInterface $paginator */
        $paginator = $gridView->getData();
        Assert::isInstanceOf($paginator, PagerfantaInterface::class);

        for ($currentPage = 1; $currentPage <= $paginator->getNbPages(); ++$currentPage) {
            $paginator->setCurrentPage($currentPage);
            $this->writePageResults($writer, $fields, $gridView, $paginator->getCurrentPageResults());
        }
    }

    /**
     * @param Field[] $fields
     * @param iterable<object> $pageResults
     */
    private function writePageResults(Writer $writer, array $fields, GridViewInterface $gridView, iterable $pageResults): void
    {
        foreach ($pageResults as $resource) {
            $rows = [];
            foreach ($fields as $field) {
                $rows[] = $this->getFieldValue($gridView, $field, $resource);
            }
            $writer->insertOne($rows);
        }
    }

    private function getFieldValue(GridViewInterface $gridView, Field $field, object $data): string
    {
        $renderedData = $this->gridRenderer->renderField($gridView, $field, $data);
        $renderedData = str_replace(\PHP_EOL, '', $renderedData);

        return trim(strip_tags($renderedData));
    }

    /**
     * @param Field[] $fields
     *
     * @return Field[]
     */
    private function sortFields(array $fields): array
    {
        $sortedFields = $fields;

        uasort($sortedFields, fn (Field $fieldA, Field $fieldB) => $fieldA->getPosition() <=> $fieldB->getPosition());

        return $sortedFields;
    }
}
```

{% endcode %}

## Configure a new operation

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Entity;

use App\Shared\Infrastructure\Sylius\Resource\ExportGridToCsvResponder;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    section: 'admin',
    templatesDir: '@SyliusAdminUi/crud',
    routePrefix: '/admin',
    operations: [
        new Index(
            grid: BookGrid::class,
        ),
        new Index(
            shortName: 'export',
            responder: ExportGridToCsvResponder::class,
            grid: BookGrid::class,
        ),
    ],
)]
class Book implements ResourceInterface
{
}
```

{% endcode %}

## Configure the grid

{% code title="src/Grid/BookGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use Sylius\Bundle\GridBundle\Builder\Action\Action;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    // ...
)]
final class BookGrid extends AbstractGrid
{
    #[\Override]
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            // ...
            ->withMainActions(
                // ...
                Action::create('export', 'export')
                    // Optional, you can configure it globally instead.
                    ->setTemplate('shared/grid/action/export.html.twig')
                ,
            )
        ;
    }
}
```

{% endcode %}

## Create the export action Twig template

You can configure the template for the export action

{% code title="templates/shared/grid/action/export.html.twig" lineNumbers="true" %}

```twig
{% set path = options.link.url|default(path(options.link.route|default(grid.requestConfiguration.getRouteName('export')), options.link.parameters|default([]))) %}

{% set message = action.label %}
{% if message is empty %}
    {% set message = 'app.ui.export' %}
{% endif %}

<a href="{{ path }}?{{ app.request.query.all()|url_encode }}" class="btn">
    {{ ux_icon(action.icon|default('iwwa:csv'), {class: 'icon dropdown-item-icon'}) }}
    {{ message|trans }}
</a>
```

{% endcode %}

## Configure the translation key

In the export action Twig template, we have introduced the `app.ui.export` translation key. So we need to configure its translation.

{% code title="translations/messages.en.yaml" lineNumbers="true" %}

```yaml
app:
    ui:
        # ...
        export: Export
```

{% endcode %}

## Global config template for export actions

To avoid repeating the `setTemplate` option across grid configurations, define it globally in the Grid Bundle config.

{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    templates:
        action:
            export: 'shared/grid/action/export.html.twig'
```

{% endcode %}


# How to use in a DDD architecture

In a Domain-Driven Design architecture, there are no defined structures. Here, we present a lot of tips to help you integrate the Sylius Stack components in your clean architecture.

In this cookbook, we reuse Mathias Arlaud's and Robin Chalas's [apip-ddd project](https://github.com/mtarld/apip-ddd) presented during their excellent **Domain-driven design with API Platform 3** talk.

{% embed url="<https://youtu.be/SSQal3Msi9g?si=L9u7tvhH4qGBpiqj>" %}


# Architecture overview

{% hint style="info" %}
The whole codebase of the DDD application example is available on [Github](https://github.com/loic425/sylius-stack-ddd).
{% endhint %}

Here is the folder structure of the "BookStore" domain:

```txt
src
└── BookStore
    ├── Application
    ├── Domain
    └── Infrastructure
        └── Sylius
```

* The `src/BookStore/Domain` directory contains your business code
* The `src/BookStore/Application` contains your application code that is not related to any framework/package
* the `src/BookStore/Infrastructure/Sylius` directory contains everything that is related to Sylius packages


# Resource configuration

```txt
src
└── BookStore
    ├── Application
    ├── Domain
    └── Infrastructure
        ├── Sylius
        │   └── Resource
        │       └── BookResource.php
        └── Symfony
            └── Form
                └── BookResourceType.php
```

## Define The Sylius Book Resource

{% code title="src/BookStore/Infrastructure/Sylius/Resource/BookResource.php" lineNumbers="true" %}

```php

namespace App\BookStore\Infrastructure\Sylius\Resource;

use App\BookStore\Domain\Model\Book;
use App\BookStore\Infrastructure\Symfony\Form\BookType;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Model\ResourceInterface;
use Symfony\Component\Uid\AbstractUid;
use Symfony\Component\Validator\Constraints as Assert;

#[AsResource(
    section: 'admin',
    templatesDir: '@SyliusAdminUi/crud',
    routePrefix: '/admin',
    driver: false,
)]
final class BookResource implements ResourceInterface
{
    public function __construct(
        public ?AbstractUid $id = null,

        #[Assert\NotNull(groups: ['create'])]
        #[Assert\Length(min: 1, max: 255, groups: ['create', 'Default'])]
        public ?string $name = null,

        #[Assert\NotNull(groups: ['create'])]
        #[Assert\Length(min: 1, max: 1023, groups: ['create', 'Default'])]
        public ?string $description = null,

        #[Assert\NotNull(groups: ['create'])]
        #[Assert\Length(min: 1, max: 255, groups: ['create', 'Default'])]
        public ?string $author = null,

        #[Assert\NotNull(groups: ['create'])]
        #[Assert\Length(min: 1, max: 65535, groups: ['create', 'Default'])]
        public ?string $content = null,

        #[Assert\NotNull(groups: ['create'])]
        #[Assert\PositiveOrZero(groups: ['create', 'Default'])]
        public ?int $price = null,
    ) {
    }

    public function getId(): ?AbstractUid
    {
        return $this->id;
    }

    public static function fromModel(Book $book): self
    {
        return new self(
            $book->id()->value,
            $book->name()->value,
            $book->description()->value,
            $book->author()->value,
            $book->content()->value,
            $book->price()->amount,
        );
    }
}
```

{% endcode %}

## Define The Symfony Book Resource Form Type

{% code title="src/BookStore/Infrastructure/Symfony/Form/BookResourceType.php" lineNumbers="true" %}

```php

namespace App\BookStore\Infrastructure\Symfony\Form;

use App\BookStore\Infrastructure\Sylius\Resource\BookResource;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

final class BookResourceType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name')
            ->add('author')
            ->add('price')
            ->add('description')
            ->add('content', TextareaType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => BookResource::class,
        ]);
    }
}
```

{% endcode %}


# Basic operations

In the previous chapter, we have created the Sylius resource. Now, we need to create the basic operations. To achieve that, we reuse commands & queries we already have in the Application folder to create providers & processors.

```txt
src
└── BookStore
    ├── Application
    │   ├── Command
    │   │   ├── CreateBookCommand.php
    │   │   ├── UpdateBookCommand.php
    │   │   └── DeleteBookCommand.php
    │   └── Query
    │       └── FindBookQuery.php
    ├── Domain
    └── Infrastructure
        └── Sylius
            └── State
                ├── Provider
                │   └── BookItemProvider.php
                └── Processor 
                    ├── CreateBookProcessor.php   
                    ├── UpdateBookProcessor.php
                    └── DeleteBookProcessor.php
```

## Book creation

<div data-full-width="false"><figure><img src="/files/DDwskCaqhUQgCUOZlSLi" alt="Create book resource"><figcaption></figcaption></figure></div>

In the Application folder, we already have this `CreateBookCommand`:

{% code title="src/Bookstore/Application/Command/CreateBookCommand.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\BookStore\Application\Command;

use App\BookStore\Domain\Model\Book;
use App\BookStore\Domain\ValueObject\Author;
use App\BookStore\Domain\ValueObject\BookContent;
use App\BookStore\Domain\ValueObject\BookDescription;
use App\BookStore\Domain\ValueObject\BookName;
use App\BookStore\Domain\ValueObject\Price;
use App\Shared\Application\Command\CommandInterface;

/**
 * @implements CommandInterface<Book>
 */
final readonly class CreateBookCommand implements CommandInterface
{
    public function __construct(
        public BookName $name,
        public BookDescription $description,
        public Author $author,
        public BookContent $content,
        public Price $price,
    ) {
    }
}
```

{% endcode %}

The idea is to reuse this command to create the book in the storage for your "create" operation.

### Create the CreateBookProcessor

First, we need to add the `CreateBookProcessor` in which we're going to call our `CreateBookCommand`.

{% code title="src/BookStore/Infrastructure/Sylius/State/Processor/CreateBookProcessor.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\BookStore\Infrastructure\Sylius\State\Processor;

use App\BookStore\Application\Command\CreateBookCommand;
use App\BookStore\Domain\ValueObject\Author;
use App\BookStore\Domain\ValueObject\BookContent;
use App\BookStore\Domain\ValueObject\BookDescription;
use App\BookStore\Domain\ValueObject\BookName;
use App\BookStore\Domain\ValueObject\Price;
use App\BookStore\Infrastructure\Sylius\Resource\BookResource;
use App\Shared\Application\Command\CommandBusInterface;
use Sylius\Resource\Context\Context;
use Sylius\Resource\Metadata\Operation;
use Sylius\Resource\State\ProcessorInterface;
use Webmozart\Assert\Assert;

/**
 * @implements ProcessorInterface<BookResource>
 */
final readonly class CreateBookProcessor implements ProcessorInterface
{
    public function __construct(
        private CommandBusInterface $commandBus,
    ) {
    }

    public function process(mixed $data, Operation $operation, Context $context): BookResource
    {
        Assert::isInstanceOf($data, BookResource::class);

        Assert::notNull($data->name);
        Assert::notNull($data->description);
        Assert::notNull($data->author);
        Assert::notNull($data->content);
        Assert::notNull($data->price);

        $command = new CreateBookCommand(
            new BookName($data->name),
            new BookDescription($data->description),
            new Author($data->author),
            new BookContent($data->content),
            new Price($data->price),
        );

        $model = $this->commandBus->dispatch($command);

        return BookResource::fromModel($model);
    }
}
```

{% endcode %}

### Adding the processor on the Book Resource

Then, we add the `Create` operation on our `BookResource`.

{% code title="src/BookStore/Infrastructure/Sylius/Resource/BookResource.php" lineNumbers="true" %}

```php

// ...
use App\BookStore\Infrastructure\Sylius\State\Processor\CreateBookProcessor;
use App\BookStore\Infrastructure\Symfony\Form\BookResourceType;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Model\ResourceInterface;
// ...

#[AsResource(
    // ...
    formType: BookResourceType::class, // Define the form type for all your operations
    operations: [
        new Create(
            processor: CreateBookProcessor::class, // the processor we have just created
            // formType: CreateBookResourceType::class, // Optional: define a specific form type only for the "create" operation
        ),
    ],
)]
final class BookResource implements ResourceInterface
{
    // ...
}
```

{% endcode %}

## Book edition

<div data-full-width="false"><figure><img src="/files/WG0OuWDgtiXvy9FYmoJU" alt="Update book resource"><figcaption></figcaption></figure></div>

Now, we want to be able to edit an existing book. In the Application folder, we already have this `UpdateBookCommand`:

{% code title="src/Bookstore/Application/Command/UpdateBookCommand.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\BookStore\Application\Command;

use App\BookStore\Domain\Model\Book;
use App\BookStore\Domain\ValueObject\Author;
use App\BookStore\Domain\ValueObject\BookContent;
use App\BookStore\Domain\ValueObject\BookDescription;
use App\BookStore\Domain\ValueObject\BookId;
use App\BookStore\Domain\ValueObject\BookName;
use App\BookStore\Domain\ValueObject\Price;
use App\Shared\Application\Command\CommandInterface;

/**
 * @implements CommandInterface<Book>
 */
final readonly class UpdateBookCommand implements CommandInterface
{
    public function __construct(
        public BookId $id,
        public ?BookName $name = null,
        public ?BookDescription $description = null,
        public ?Author $author = null,
        public ?BookContent $content = null,
        public ?Price $price = null,
    ) {
    }
}
```

{% endcode %}

In the same folder, we also have this existing `FindBookQuery`:

{% code title="src/BookStore/Application/Query/FindBookQuery.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\BookStore\Application\Query;

use App\BookStore\Domain\Model\Book;
use App\BookStore\Domain\ValueObject\BookId;
use App\Shared\Application\Query\QueryInterface;

/**
 * @implements QueryInterface<Book>
 */
final readonly class FindBookQuery implements QueryInterface
{
    public function __construct(
        public BookId $id,
    ) {
    }
}
```

{% endcode %}

The idea is to reuse these query and command to update the book in the storage for your "update" operation.

### Create the BookItemProvider

First, we need to create the `BookItemProvider` in order to fetch the right book.

{% code title="src/BookStore/Infrastructure/Sylius/State/Provider/BookItemProvider.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\BookStore\Infrastructure\Sylius\State\Provider;

use App\BookStore\Application\Query\FindBookQuery;
use App\BookStore\Domain\ValueObject\BookId;
use App\BookStore\Infrastructure\Sylius\Resource\BookResource;
use App\Shared\Application\Query\QueryBusInterface;
use Sylius\Resource\Context\Context;
use Sylius\Resource\Context\Option\RequestOption;
use Sylius\Resource\Metadata\Operation;
use Sylius\Resource\State\ProviderInterface;
use Symfony\Component\Uid\Uuid;

/**
 * @implements ProviderInterface<BookResource>
 */
final readonly class BookItemProvider implements ProviderInterface
{
    public function __construct(
        private QueryBusInterface $queryBus,
    ) {
    }

    public function provide(Operation $operation, Context $context): object|array|null
    {
        $id = $context->get(RequestOption::class)
            ?->request()
            ->attributes
            ->getString('id')
        ;

        $model = $this->queryBus->ask(new FindBookQuery(new BookId(Uuid::fromString($id))));

        return BookResource::fromModel($model);
    }
}
```

{% endcode %}

### Create the UpdateBookProcessor

We also need to create the `UpdateBookProcessor` where we're going to call our `UpdateBookCommand`.

{% code title="src/BookStore/Infrastructure/Sylius/State/Processor/UpdateBookProcessor.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\BookStore\Infrastructure\Sylius\State\Processor;

use App\BookStore\Application\Command\UpdateBookCommand;
use App\BookStore\Domain\ValueObject\Author;
use App\BookStore\Domain\ValueObject\BookContent;
use App\BookStore\Domain\ValueObject\BookDescription;
use App\BookStore\Domain\ValueObject\BookId;
use App\BookStore\Domain\ValueObject\BookName;
use App\BookStore\Domain\ValueObject\Price;
use App\BookStore\Infrastructure\Sylius\Resource\BookResource;
use App\Shared\Application\Command\CommandBusInterface;
use Sylius\Resource\Context\Context;
use Sylius\Resource\Metadata\Operation;
use Sylius\Resource\State\ProcessorInterface;
use Webmozart\Assert\Assert;

/**
 * @implements ProcessorInterface<BookResource>
 */
final readonly class UpdateBookProcessor implements ProcessorInterface
{
    public function __construct(
        private CommandBusInterface $commandBus,
    ) {
    }

    public function process(mixed $data, Operation $operation, Context $context): mixed
    {
        Assert::isInstanceOf($data, BookResource::class);

        $command = new UpdateBookCommand(
            new BookId($data->id),
            null !== $data->name ? new BookName($data->name) : null,
            null !== $data->description ? new BookDescription($data->description) : null,
            null !== $data->author ? new Author($data->author) : null,
            null !== $data->content ? new BookContent($data->content) : null,
            null !== $data->price ? new Price($data->price) : null,
        );

        $model = $this->commandBus->dispatch($command);

        return BookResource::fromModel($model);
    }
}
```

{% endcode %}

### Adding the provider & processor on the Book Resource

Now, we add the "update" operation on the `BookResource`.

{% code title="src/BookStore/Infrastructure/Sylius/Resource/BookResource.php" lineNumbers="true" %}

```php
// ...
use App\BookStore\Infrastructure\Sylius\State\Processor\UpdateBookProcessor;
use App\BookStore\Infrastructure\Sylius\State\Provider\BookItemProvider;
use App\BookStore\Infrastructure\Symfony\Form\BookResourceType;
use Sylius\Resource\Metadata\Update;
// ...

#[AsResource(
    // ...
    formType: BookResourceType::class, // Define the form type for all your operations
    operations: [
        // ...
        new Update(
            provider: BookItemProvider::class, // the provider we have just created
            processor: UpdateBookProcessor::class, // the processor we have just created
            // formType: UpdateBookResourceType::class, // Optional: define a specific form type only for the "update" operation
        ),
    ],
)]
final class BookResource implements ResourceInterface
{
    // ...
}
```

{% endcode %}

## Book removal

<div data-full-width="false"><figure><img src="/files/qFW2Z0Im5HSPiUX8YApT" alt="Delete book resource"><figcaption></figcaption></figure></div>

Now that we can update an existing book, we also want to be able to delete it. In the Application folder, we already have this `DeleteBookCommand`:

{% code title="src/BookStore/Application/Command/DeleteBookCommand.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\BookStore\Application\Command;

use App\BookStore\Domain\ValueObject\BookId;
use App\Shared\Application\Command\CommandInterface;

/**
 * @implements CommandInterface<void>
 */
final readonly class DeleteBookCommand implements CommandInterface
{
    public function __construct(
        public BookId $id,
    ) {
    }
}
```

{% endcode %}

### Create the DeleteBookProcessor

We need to create the `DeleteBookProcessor`.

{% code title="src/BookStore/Infrastructure/Sylius/State/Processor/DeleteBookProcessor.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\BookStore\Infrastructure\Sylius\State\Processor;

use App\BookStore\Application\Command\DeleteBookCommand;
use App\BookStore\Domain\ValueObject\BookId;
use App\BookStore\Infrastructure\Sylius\Resource\BookResource;
use App\Shared\Application\Command\CommandBusInterface;
use Sylius\Resource\Context\Context;
use Sylius\Resource\Metadata\Operation;
use Sylius\Resource\State\ProcessorInterface;
use Webmozart\Assert\Assert;

/**
 * @implements ProcessorInterface<null>
 */
final readonly class DeleteBookProcessor implements ProcessorInterface
{
    public function __construct(
        private CommandBusInterface $commandBus,
    ) {
    }

    public function process(mixed $data, Operation $operation, Context $context): mixed
    {
        Assert::isInstanceOf($data, BookResource::class);
        $this->commandBus->dispatch(new DeleteBookCommand(new BookId($data->id)));

        return null;
    }
}
```

{% endcode %}

### Adding the processor on the Book Resource

And then we create the "delete" operation on the BookResource.

{% code title="src/BookStore/Infrastructure/Sylius/Resource/BookResource.php" lineNumbers="true" %}

```php
// ...
use App\BookStore\Infrastructure\Sylius\State\Processor\DeleteBookProcessor;
use App\BookStore\Infrastructure\Sylius\State\Provider\BookItemProvider;
use Sylius\Resource\Metadata\Delete;
// ...

#[AsResource(
    // ...
    operations: [
        // ...
        new Delete(
            provider: BookItemProvider::class, // the provider we have already created in book creation
            processor: DeleteBookProcessor::class, // the processor we have just created
        ),
    ],
)]
final class BookResource implements ResourceInterface
{
    // ...
}
```

{% endcode %}


# Operation using a grid

In previous chapters, we have created the Sylius resource and basic operations. Now we need to create the index operation using a Grid. To achieve that, we reuse the query we already have in the Application folder to create a grid provider.

```txt
src
├── BookStore
│   ├── Application
│   │   └── Query
│   │       ├── FindBooksQuery.php
│   │       └── FindBooksQueryHandler.php
│   ├── Domain
│   └── Infrastructure
│       └── Sylius
│           └── Grid
│               ├── Filter
│               │   ├── AuthorFilter.php
│               │   └── AuthorFilterType.php
│               ├── BookGrid.php
│               └── BookGridProvider.php
└── Shared
    └── Infrastructure
        └── Sylius
            └── Grid
                └── GridPageResolver.php
```

## Book list

<div data-full-width="false"><figure><img src="/files/Zo6JtujlbNkXG7ECkr2H" alt="List of book resources"><figcaption></figcaption></figure></div>

In the Application folder, we already have this `FindBooksQuery`:

{% code title="src/Bookstore/Application/Query/FindBooksQuery.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\BookStore\Application\Query;

use App\BookStore\Domain\Repository\BookRepositoryInterface;
use App\BookStore\Domain\ValueObject\Author;
use App\Shared\Application\Query\QueryInterface;

/**
 * @implements QueryInterface<BookRepositoryInterface>
 */
final readonly class FindBooksQuery implements QueryInterface
{
    public function __construct(
        public ?Author $author = null,
        public ?int $page = null,
        public ?int $itemsPerPage = null,
        public ?bool $alphabeticalSortingAsc = null,
        public ?bool $alphabeticalSortingDesc = null,
    ) {
    }
}
```

{% endcode %}

And its query handler:

{% code title="src/Bookstore/Application/Query/FindBooksQueryHandler.php" lineNumbers="true" %}

```php

namespace App\BookStore\Application\Query;

use App\BookStore\Domain\Repository\BookRepositoryInterface;
use App\Shared\Application\Query\AsQueryHandler;

#[AsQueryHandler]
final readonly class FindBooksQueryHandler
{
    public function __construct(private BookRepositoryInterface $bookRepository)
    {
    }

    public function __invoke(FindBooksQuery $query): BookRepositoryInterface
    {
        $bookRepository = $this->bookRepository;

        if (null !== $query->author) {
            $bookRepository = $bookRepository->withAuthor($query->author);
        }

        if (null !== $query->page && null !== $query->itemsPerPage) {
            $bookRepository = $bookRepository->withPagination($query->page, $query->itemsPerPage);
        }

        if ($query->alphabeticalSortingAsc) {
            $bookRepository = $bookRepository->withAscendingAlphabeticalSorting();
        }

        if ($query->alphabeticalSortingDesc) {
            $bookRepository = $bookRepository->withDescendingAlphabeticalSorting();
        }

        return $bookRepository;
    }
}
```

{% endcode %}

The idea is to reuse this query to list book from your storage for your "index" operation.

### Create a page helper

To resolve current page and items per page in the grid provider, we can use this helper:

{% code title="src/Shared/Infrastructure/Sylius/Grid/GridPageResolver.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\Shared\Infrastructure\Sylius\Grid;

use Sylius\Component\Grid\Definition\Grid;
use Sylius\Component\Grid\Parameters;

class GridPageResolver
{
    public static function getCurrentPage(Grid $grid, Parameters $parameters): int
    {
        return $parameters->has('page') ? (int) $parameters->get('page') : 1;
    }

    public static function getItemsPerPage(Grid $grid, Parameters $parameters): int
    {
        return $parameters->has('limit') ? (int) $parameters->get('limit') : $grid->getLimits()[0] ?? 10;
    }
}
```

{% endcode %}

### Create the BookGridProvider

First, we need to create the `BookGridProvider`.

{% code title="src/BookStore/Infrastructure/Sylius/Grid/BookGridProvider.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\BookStore\Infrastructure\Sylius\Grid;

use App\BookStore\Application\Query\FindBooksQuery;
use App\BookStore\Domain\ValueObject\Author;
use App\BookStore\Infrastructure\Sylius\Resource\BookResource;
use App\Shared\Application\Query\QueryBusInterface;
use App\Shared\Infrastructure\Sylius\Grid\GridPageResolver;
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;
use Webmozart\Assert\Assert;

final readonly class BookGridProvider implements DataProviderInterface
{
    public function __construct(
        private QueryBusInterface $queryBus,
    ) {
    }

    public function getData(Grid $grid, Parameters $parameters): PagerfantaInterface
    {
        $models = $this->queryBus->ask(new FindBooksQuery(
            page: GridPageResolver::getCurrentPage($grid, $parameters),
            itemsPerPage: GridPageResolver::getItemsPerPage($grid, $parameters),
        ));

        $data = [];
        foreach ($models as $model) {
            $data[] = BookResource::fromModel($model);
        }
        
        $paginator = $models->paginator();
        Assert::notNull($paginator);

        return new Pagerfanta(new FixedAdapter($paginator->getTotalItems(), $data));
    }
}
```

{% endcode %}

### Create the BookGrid

Now, we need to create the `BookGrid`.

{% code title="src/BookStore/Infrastructure/Sylius/Grid/BookGrid.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\BookStore\Infrastructure\Sylius\Grid;

use App\BookStore\Infrastructure\Sylius\Resource\BookResource;
use Sylius\Bundle\GridBundle\Builder\Action\CreateAction;
use Sylius\Bundle\GridBundle\Builder\Action\DeleteAction;
use Sylius\Bundle\GridBundle\Builder\Action\UpdateAction;
use Sylius\Bundle\GridBundle\Builder\Field\StringField;
use Sylius\Bundle\GridBundle\Builder\Field\TwigField;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Bundle\GridBundle\Grid\ResourceAwareGridInterface;

final class BookGrid extends AbstractGrid implements ResourceAwareGridInterface
{
    public static function getName(): string
    {
        return self::class;
    }

    public function buildGrid(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->setProvider(BookGridProvider::class) // The Grid provider we have just created
            ->withFields(
                StringField::create('name'),
                StringField::create('author'),
            )
            ->withMainActions(
                CreateAction::create(),
            )
            ->withItemActions(
                UpdateAction::create(),
                DeleteAction::create(),
            )
            ->withBulkActions(
                DeleteAction::create(),
            )
        ;
    }

    public function getResourceClass(): string
    {
        return BookResource::class;
    }
}
```

{% endcode %}

### Add the grid on the Book Resource

Now that we have a grid, let's add it to the "index" operation on our `BookResource`.

{% code title="src/BookStore/Infrastructure/Sylius/Resource/BookResource.php" lineNumbers="true" %}

```php
// ...
use App\BookStore\Infrastructure\Sylius\Grid\BookGrid;
use App\BookStore\Infrastructure\Symfony\Form\BookResourceType;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Index;
use Sylius\Resource\Model\ResourceInterface;
// ...

#[AsResource(
    // ...
    formType: BookResourceType::class, // Define the form type for all your operations
    operations: [
        // ...
        new Index(
            grid: BookGrid::class, // the grid we have just created
        ),
    ],
)]
final class BookResource implements ResourceInterface
{
    // ...
}
```

{% endcode %}

### Create the Author filter type

Let's imagine we want to be able to filter books by their author within our grid. First, we need to create a Symfony form type for our custom author filter.

{% code title="src/BookStore/Infrastructure/Sylius/Grid/Filter/AuthorFilterType.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\BookStore\Infrastructure\Sylius\Grid\Filter;

use App\BookStore\Application\Query\FindBooksQuery;
use App\BookStore\Domain\Model\Book;
use App\Shared\Application\Query\QueryBusInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\OptionsResolver;

final class AuthorFilterType extends AbstractType
{
    public function __construct(
        private readonly QueryBusInterface $queryBus,
    ) {
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'placeholder' => 'sylius.ui.all',
            'choices' => $this->getChoices(),
        ]);
    }

    public function getParent(): string
    {
        return ChoiceType::class;
    }

    private function getChoices(): array
    {
        // We do not have any findAuthorsQuery
        // Authors are stored in the Book resource
        $models = $this->queryBus->ask(new FindBooksQuery());

        $choices = [];

        /** @var Book $model */
        foreach ($models as $model) {
            $choices[$model->author()->value] = $model->author()->value;
        }

        ksort($choices);

        return $choices;
    }
}
```

{% endcode %}

### Create the Author filter

{% code title="src/BookStore/Infrastructure/Sylius/Grid/Filter/AuthorFilter.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\BookStore\Infrastructure\Sylius\Grid\Filter;

use Sylius\Component\Grid\Data\DataSourceInterface;
use Sylius\Component\Grid\Filtering\ConfigurableFilterInterface;

final readonly class AuthorFilter implements ConfigurableFilterInterface
{
    public function apply(DataSourceInterface $dataSource, string $name, $data, array $options): void
    {
        throw new \RuntimeException('Not implemented'); // We cannot use the DataSource generic abstraction
    }

    public static function getFormType(): string
    {
        return AuthorFilterType::class; // The Symfony form type we have just created
    }

    public static function getType(): string
    {
        return self::class;
    }
} 
```

{% endcode %}

### Use the Author Filter on the Grid provider

{% code title="src/BookStore/Infrastructure/Sylius/Grid/BookGridProvider.php" lineNumbers="true" %}

```php
final readonly class BookGridProvider implements DataProviderInterface
{
    // ...

    public function getData(Grid $grid, Parameters $parameters): PagerfantaInterface
    {
        /** @var array<string, string> $criteria */
        $criteria = $parameters->get('criteria', []); // Getting the criteria from query params

        $models = $this->queryBus->ask(new FindBooksQuery(
            author: !empty($criteria['author'] ?? null) ? new Author($criteria['author']) : null,
            page: GridPageResolver::getCurrentPage($grid, $parameters),
            itemsPerPage: GridPageResolver::getItemsPerPage($grid, $parameters),
        ));

        // ...
    }
}
```

{% endcode %}


# Resource Bundle documentation

## SyliusResourceBundle

The **Sylius Resource Bundle** provides a powerful and extensible foundation for exposing your **business resources** (entities, aggregates, etc.) in a declarative way.\
Rather than generating controllers or relying on rigid admin generators, it offers a flexible architecture that lets you focus on your domain model while the bundle handles the boilerplate.

A *Resource* is any business object you want to expose — for example, a `Product`, `Order`, or `UserProfile`.

Each resource can define a set of **operations** — actions that can be performed on it.\
Typical operations include `index`, `show`, `create`, `update`, and `delete`, but you can also define custom, domain-specific operations too.\
The bundle orchestrates each operation through a well-defined lifecycle involving **providers**, **processors**, and **responders**:

* **Providers** are responsible for loading or creating the resource object and **validating it** (ensuring the object is consistent before any business logic is applied).
  * Example: load from Doctrine, create a new instance, hydrate from request data, validate the object, or fetch from an external API.
* **Processors** handle the business logic or persistence layer (e.g. saving, executing domain services, dispatching events).
* **Responders** produce the final response (e.g. rendering a template).

This architecture allows you to use the bundle in two main ways:

* **Rapid Application Mode (RAD)** – perfect for quick CRUD setup with Doctrine ORM. You define your entity, mark it as a resource, and everything just works.
* **Domain-Driven Design / Advanced Mode** – where you control how data is provided and processed by writing your own providers and processors.

In short, the Sylius Resource Bundle is both **declarative** and **extensible** — define your resources and their operations, and let the framework handle the rest, while still giving you full control over the domain logic when you need it.

### Resource system for Symfony applications.

* [Installation](/resource/index/installation)

## New documentation

* [Resource lifecycle](/resource/index/lifecycle)
* [Create a new resource](/resource/index/create_new_resource)
* [Configure your resource](/resource/index/configure_your_resource)
* [Configure your operations](/resource/index/configure_your_operations)
* [Validation](/resource/index/validation)
* [Redirect](/resource/index/redirect)
* [Resource Factories](/resource/index/resource_factories)
* [Providers](/resource/index/providers)
* [Processors](/resource/index/processors)
* [Responders](/resource/index/responders)

## Deprecated documentation

* [Legacy Resource Documentation](/resource/index/index)

### Learn more

* [Resource Layer in the Sylius platform](https://docs.sylius.com/the-book/architecture/resource-layer) - concept documentation


# Installation

We assume you're familiar with [Composer](http://packagist.org), a dependency manager for PHP. Use the following command to add the bundle to your `composer.json` and download the package.

If you have [Composer installed globally](http://getcomposer.org/doc/00-intro.md#globally).

```bash
composer require sylius/resource-bundle
```

Otherwise, you have to download .phar file.

```bash
curl -sS https://getcomposer.org/installer | php
php composer.phar require sylius/resource-bundle
```

## Adding Required Bundles to The Kernel

You need to enable the bundle and its dependencies in the kernel:

{% code title="config/bundles.php" %}

```php
return [
    new Sylius\Bundle\ResourceBundle\SyliusResourceBundle(),
    new BabDev\PagerfantaBundle\BabDevPagerfantaBundle(),
];
```

{% endcode %}

Configure your mapping paths for your resources :

{% tabs %}
{% tab title="PHP" %}
{% code title="config/packages/sylius\_resource.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    $containerConfigurator->extension(
    namespace: 'sylius_resource',
     config: [
        'mapping' => [
            'paths' => [
                '%kernel.project_dir%/src/Entity',
            ],
        ],
        'resources' => null,
    ]);
};
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_resource.yaml" lineNumbers="true" %}

```yaml
sylius_resource:
    mapping:
        paths:
            - '%kernel.project_dir%/src/Entity'
```

{% endcode %}
{% endtab %}
{% endtabs %}

Configure the routing

{% tabs %}
{% tab title="PHP" %}
{% code title="config/packages/sylius\_resource.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;

return static function (RoutingConfigurator $routingConfigurator): void {
    $routingConfigurator->import(resource: 'sylius.symfony.routing.loader.resource', type: 'service');
};
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/routes/sylius\_resource.yaml" %}

```yaml
sylius_resource_routes:
    resource: 'sylius.symfony.routing.loader.resource'
    type: service
```

{% endcode %}
{% endtab %}
{% endtabs %}

That's it! Now you can configure your first resource.


# Resource Lifecycle

Each operation on a resource follows a well-defined lifecycle.\
This flow ensures clear separation of concerns between reading data, applying business logic, and producing the final response.

{% @mermaid/diagram content="flowchart TD
A\[Request] --> B\[Routing & Metadata]
B --> C\[Provider]
C --> D\[Processor]
D --> E\[Responder]
E --> F\[Response]

```
subgraph ProviderPhase [Provider Phase]
    direction LR
    C1[Load existing resource]
    C2[Create new instance]
    C3[Hydrate resource from request]
    C4[Validate resource]
end

subgraph ProcessorPhase [Processor Phase]
    direction LR
    D1[Apply business logic]
    D2[Persist resource]
    D3[Dispatch domain events]
end

subgraph ResponderPhase [Responder Phase]
    direction LR
    E1[Render template or redirect]
    E2[Return HTTP response]
end

C --> ProviderPhase
D --> ProcessorPhase
E --> ResponderPhase

style ProviderPhase fill:#f4f8ff,stroke:#93c5fd,stroke-width:1px
style ProcessorPhase fill:#fef9c3,stroke:#facc15,stroke-width:1px
style ResponderPhase fill:#fef2f2,stroke:#f87171,stroke-width:1px" %}
```

## Step-by-step explanation

### Routing & Metadata

The request is matched to a `Resource` and an `Operation` using metadata collected from attributes (eg `#[AsResource]`).\
This step defines which provider, processor, and responder should handle the request.

### Provider Phase

The provider is responsible for loading or creating the resource object, hydrating it from the request, and validating it before any business logic is applied.

**Example:** load from Doctrine, create a new instance, populate from request data, validate, or fetch from an external API.

### Processor Phase

The processor applies the business logic of the operation.

**Example:** persist the entity, execute domain services, or dispatch events.

You can use the default Doctrine processor, or implement your own for DDD use cases.

### Responder Phase

The responder produces the final output — it can render a Twig template or redirect to another route.


# Create new resource

As an example, let's create a Book entity:

* [Create the entity](#create-the-entity)
* [Configure the BookRepository](#configure-the-bookrepository)

## Create the entity

To create a new entity, we need to run the following command:

```shell
$ bin/console make:entity 'App\Entity\Book'
```

This command is interactive: it will guide you through the process of adding all the fields you need. Use the following answers (most of them are the defaults, so you can hit the "Enter" key to use them):

|   Name | Author | Description | Price |
| -----: | ------ | :---------: | ----- |
| String | String |     Text    | Float |
|    255 | 255    |      /      | /     |
|     No | No     |      No     | No    |

***

Here is the full output when running the command:

```
created: src/Entity/Book.php
created: src/Repository/BookRepository.php

Entity generated! Now let's add some fields!
You can always add more fields later manually or by re-running this command.

New property name (press <return> to stop adding fields):
> name

Field type (enter ? to see all types) [string]:
> 

Field length [255]:
>

Can this field be null in the database (nullable) (yes/no) [no]:
> 

updated: src/Entity/Book.php

Add another property? Enter the property name (or press <return> to stop adding fields):
> author

Field type (enter ? to see all types) [string]:
> 

Field length [255]:
>

Can this field be null in the database (nullable) (yes/no) [no]:
> 

updated: src/Entity/Book.php

Add another property? Enter the property name (or press <return> to stop adding fields):
> description

Field type (enter ? to see all types) [string]:
> text

Can this field be null in the database (nullable) (yes/no) [no]:
> 

updated: src/Entity/Book.php

Add another property? Enter the property name (or press <return> to stop adding fields):
> price

Field type (enter ? to see all types) [string]:
> float

Can this field be null in the database (nullable) (yes/no) [no]:
> 

updated: src/Entity/Book.php

Add another property? Enter the property name (or press <return> to stop adding fields):
> 


       
Success! 
       

Next: When you're ready, create a migration with php bin/console make:migration

```

## Configure the BookRepository

The command also generated a Doctrine repository class.

{% code title="App\Repository\BookRepository.php" lineNumbers="true" %}

```php
<?php

namespace App\Repository;

use App\Entity\Book;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/**
 * [...]
 */
class BookRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Book::class);
    }
    
    // [...]
}
```

{% endcode %}

The generated code is not fully compatible with Sylius Resource yet, so we need to make a few changes. Please add the `createPaginator` method using the `Sylius\Bundle\ResourceBundle\Doctrine\ORM\CreatePaginatorTrait` trait

Your repository should look like this:

{% code title="App\Repository\BookRepository.php" lineNumbers="true" %}

```php
<?php

namespace App\Repository;

use App\Entity\Book;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Sylius\Bundle\ResourceBundle\Doctrine\ORM\CreatePaginatorTrait;

/**
 * [...]
 */
class BookRepository extends ServiceEntityRepository
{
    use CreatePaginatorTrait;

    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Book::class);
    }
}
```

{% endcode %}


# Configure your resource

Read the previous chapter to [create a new resource](/resource/index/create_new_resource). In order for your resource to truly become a `Sylius Resource`, you will need to configure a couple of things.

* [Configure your resource](#configure-your-resource)
  * [Implement the Resource interface](#implement-the-resource-interface)
  * [Use the Resource attribute](#use-the-resource-attribute)
  * [Advanced configuration](#advanced-configuration)
    * [Configure the resource name](#configure-the-resource-name)
    * [Configure the resource plural name](#configure-the-resource-plural-name)
    * [Configure the resource vars](#configure-the-resource-vars)

## Implement the Resource interface

First, to declare your resource as a Sylius Resource, implement the `Sylius\Component\Resource\Model\ResourceInterface`, which requires defining a getId() method to uniquely identify the resource.

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php

namespace App\Entity;

class Book implements ResourceInterface
{
    public function getId(): int
    {
        return $this->id;
    }
}
```

{% endcode %}

## Register your resource using the AsResource attribute

Next, add the `#[AsResource]` PHP attribute to your Doctrine entity to register it as a Sylius resource.

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource]
class Book implements ResourceInterface
{
}

```

{% endcode %}

Run the following command to verify that your resource is correctly configured.

```shell
$ bin/console sylius:debug:resource 'App\Entity\Book'
```

```
Resource Metadata
-----------------

 ------------------------ ------------------- 
  Option                   Value              
 ------------------------ ------------------- 
  alias                    "app.book"         
  section                  null               
  formType                 null               
  templatesDir             null               
  routePrefix              null               
  name                     "book"             
  pluralName               null               
  applicationName          "app"              
  identifier               null               
  normalizationContext     null               
  denormalizationContext   null               
  validationContext        null               
  class                    "App\Entity\Book"  
  driver                   null               
  vars                     null               
 ------------------------ -------------------
 
 [...]
```

By default, the alias for your Sylius resource will be `app.book`, which combines the application name and the resource name with this format : `{application}.{resource}`.

## Register your resource using an external PHP file

The alternative to register your resource is to use an external PHP file.

First you need to configure your custom directory for your resource configuration files.

```yaml
sylius_resource:
    mapping:
        imports:
            - '%kernel.project_dir%/config/sylius/resources'
```

Now, you are able to create your custom resource configuration file.

{% code title="config/sylius/resources/book.php" lineNumbers="true" %}

```php
use App\Entity\Book;
use Sylius\Resource\Metadata\ResourceMetadata;

return (new ResourceMetadata())
    ->withClass(Book::class)
;
```

{% endcode %}

## Advanced configuration

### Configure the resource name

You can override your resource's name via the `name` parameter of the `AsResource` PHP attribute.

{% tabs %}
{% tab title="PHP attributes" %}
{% code title="src/Entity/Order.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(name: 'cart')]
class Order implements ResourceInterface
{
}
```

{% endcode %}
{% endtab %}

{% tab title="External PHP file" %}
{% code title="config/sylius/resources/order.php" lineNumbers="true" %}

```php
use App\Entity\Order;
use Sylius\Resource\Metadata\ResourceMetadata;

return (new ResourceMetadata())
    ->withClass(Order::class)
    ->withName('cart')
;
```

{% endcode %}
{% endtab %}
{% endtabs %}

In this example, the `order` variable is replaced with `cart` in your Twig templates. As a result, for the `show` operation, the following Twig variables will be available within the template:

| Name               | Type                                      |
| ------------------ | ----------------------------------------- |
| resource           | App\Entity\Order                          |
| cart               | App\Entity\Order                          |
| operation          | Sylius\Resource\Metadata\Show             |
| resource\_metadata | Sylius\Resource\Metadata\ResourceMetadata |
| app                | Symfony\Bridge\Twig\AppVariable           |

### Configure the resource plural name

You can override your resource's plural name via the `pluralName` parameter of the `AsResource` PHP attribute.

{% tabs %}
{% tab title="PHP attributes" %}
{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(pluralName: 'library')]
class Book implements ResourceInterface
{
}
```

{% endcode %}
{% endtab %}

{% tab title="External PHP file" %}
{% code title="config/sylius/resources/book.php" lineNumbers="true" %}

```php
use App\Entity\Book;
use Sylius\Resource\Metadata\ResourceMetadata;

return (new ResourceMetadata())
    ->withClass(Book::class)
    ->withPluralName('library')
;
```

{% endcode %}
{% endtab %}
{% endtabs %}

In this example, the `books` variable is replaced with `library` in your Twig templates. As a result, for the `index` operation, the following Twig variables will be available within the template:

| Name               | Type                                      |
| ------------------ | ----------------------------------------- |
| resources          | Pagerfanta\Pagerfanta                     |
| library            | Pagerfanta\Pagerfanta                     |
| operation          | Sylius\Resource\Metadata\Index            |
| resource\_metadata | Sylius\Resource\Metadata\ResourceMetadata |
| app                | Symfony\Bridge\Twig\AppVariable           |

### Configure additional resource vars

You can define simple variables within the `AsResource` attribute via the `vars` parameter.

{% tabs %}
{% tab title="PHP attributes" %}
{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    vars: [
        'header' => 'Library', 
        'subheader' => 'Managing your library',
    ],
)]
class Book implements ResourceInterface
{
}
```

{% endcode %}
{% endtab %}

{% tab title="External PHP file" %}
{% code title="config/sylius/resources/book.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

use App\Entity\Book;
use Sylius\Resource\Metadata\ResourceMetadata;

return (new ResourceMetadata())
    ->withClass(Book::class)
    ->withVars([
        'header' => 'Library', 
        'subheader' => 'Managing your library',
    ])
;
```

{% endcode %}
{% endtab %}
{% endtabs %}

You can then access these variables in your Twig templates. These variables will be available for every operation associated with this resource.

```html
<h1>{{ operation.vars.header }}</h1>
<h2>{{ operation.vars.subheader }}</h2>
```


# Configure your operations

Read the previous chapter to [configure your resource](/resource/index/configure_your_resource).

Now, with your fresh new resource, you have to define the operations that you need to implement. There are some basic CRUD operations and more.

* [Configure your operations](#configure-your-operations)
  * [Basic operations](#basic-operations)
    * [Index operation](#index-operation)
    * [Use a grid for your index operation](#use-a-grid-for-your-index-operation)
    * [Show operation](#show-operation)
    * [Create operation](#create-operation)
    * [Update operation](#update-operation)
    * [Delete operation](#delete-operation)
    * [Bulk delete operation](#bulk-delete-operation)
    * [State machine operation](#state-machine-operation)
  * [Advanced configuration](#advanced-configuration)
    * [Configure the path](#configure-the-path)
    * [Configure the short name](#configure-the-short-name)
    * [Configure the templates' dir](#configure-the-templates-dir)
    * [Configure the routes' prefix](#configure-the-routes-prefix)
    * [Configure the section](#configure-the-section)
    * [Configure the resource identifier](#configure-the-resource-identifier)
    * [Configure the vars](#configure-the-vars)

## Basic operations

### Index operation

`Index` operation allows to browse all items of your resource.

{% tabs %}
{% tab title="PHP attributes" %}
{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Index;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    operations: [
        new Index(),
    ],
)]
class Book implements ResourceInterface
{
}
```

{% endcode %}
{% endtab %}

{% tab title="External PHP file" %}
{% code title="config/sylius/resources/book.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

use App\Entity\Book;
use Sylius\Resource\Metadata\Index;
use Sylius\Resource\Metadata\Operations;
use Sylius\Resource\Metadata\ResourceMetadata;

return (new ResourceMetadata())
    ->withClass(Book::class)
    ->withOperations(
        new Operations([
            new Index(),
        ])
    )
;
```

{% endcode %}
{% endtab %}
{% endtabs %}

It will configure this route for your `index` operation.

| Name             | Method | Path   |
| ---------------- | ------ | ------ |
| app\_book\_index | GET    | /books |

On your Twig template, these variables are available

| Name               | Type                                      |
| ------------------ | ----------------------------------------- |
| resources          | Pagerfanta\Pagerfanta                     |
| books              | Pagerfanta\Pagerfanta                     |
| operation          | Sylius\Resource\Metadata\Index            |
| resource\_metadata | Sylius\Resource\Metadata\ResourceMetadata |
| app                | Symfony\Bridge\Twig\AppVariable           |

### Use a grid for your index operation

To use a grid for you operation, you need to install the [Sylius grid package](https://github.com/Sylius/SyliusGridBundle/)

{% tabs %}
{% tab title="PHP attributes" %}
{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
namespace App\Entity;

use App\Grid\BookGrid;
use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Index;

#[AsResource(
    operations: [
        // You can use either the FQCN of your grid
        new Index(grid: BookGrid::class),
        // Or you can use the grid name
        new Index(grid: 'app_book'),
    ],
)]
class Book implements ResourceInterface
{
}
```

{% endcode %}
{% endtab %}

{% tab title="External PHP file" %}
{% code title="config/sylius/resources/book.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

use App\Entity\Book;
use Sylius\Resource\Metadata\Index;
use Sylius\Resource\Metadata\Operations;
use Sylius\Resource\Metadata\ResourceMetadata;

return (new ResourceMetadata())
    ->withClass(Book::class)
    ->withOperations(
        new Operations([
            // You can use either the FQCN of your grid
            new Index(grid: BookGrid::class),
            // Or you can use the grid name
            new Index(grid: 'app_book'),
        ])    
    )
;
```

{% endcode %}
{% endtab %}
{% endtabs %}

On your Twig template, these variables are available

| Name               | Type                                                    |
| ------------------ | ------------------------------------------------------- |
| resources          | Sylius\Bundle\ResourceBundle\Grid\View\ResourceGridView |
| books              | Sylius\Bundle\ResourceBundle\Grid\View\ResourceGridView |
| operation          | Sylius\Resource\Metadata\Index                          |
| resource\_metadata | Sylius\Resource\Metadata\ResourceMetadata               |
| app                | Symfony\Bridge\Twig\AppVariable                         |

The iterator for your books will be available as `books.data` or `resources.data`.

### Show operation

`Show` operation allows to view details of an item.

{% tabs %}
{% tab title="PHP attributes" %}
{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Show;

#[AsResource(
    operations: [
        new Show(),
    ],
)
class Book implements ResourceInterface
{
}
```

{% endcode %}
{% endtab %}

{% tab title="External PHP file" %}
{% code title="config/sylius/resources/book.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

use App\Entity\Book;
use Sylius\Resource\Metadata\Operations;
use Sylius\Resource\Metadata\ResourceMetadata;
use Sylius\Resource\Metadata\Show;

return (new ResourceMetadata())
    ->withClass(Book::class)
    ->withOperations(
        new Operations([
            new Show(),
        ])    
    )
;
```

{% endcode %}
{% endtab %}
{% endtabs %}

It will configure this route for your `show` operation.

| Name            | Method | Path        |
| --------------- | ------ | ----------- |
| app\_book\_show | GET    | /books/{id} |

On your Twig template, these variables are available

| Name               | Type                                      |
| ------------------ | ----------------------------------------- |
| resource           | App\Entity\Book                           |
| book               | App\Entity\Book                           |
| operation          | Sylius\Resource\Metadata\Show             |
| resource\_metadata | Sylius\Resource\Metadata\ResourceMetadata |
| app                | Symfony\Bridge\Twig\AppVariable           |

### Create operation

`Create` operation allows to add a new item of your resource.

{% tabs %}
{% tab title="PHP attributes" %}
{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Create;

#[AsResource(
    operations: [
        new Create(),
    ],
)
class Book implements ResourceInterface
{
}
```

{% endcode %}
{% endtab %}

{% tab title="External PHP file" %}
{% code title="config/sylius/resources/book.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

use App\Entity\Book;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Metadata\Operations;
use Sylius\Resource\Metadata\ResourceMetadata;

return (new ResourceMetadata())
    ->withClass(Book::class)
    ->withOperations(
        new Operations([
            new Create(),
        ])    
    )
;
```

{% endcode %}
{% endtab %}
{% endtabs %}

It will configure this route for your `create` operation.

| Name              | Method    | Path       |
| ----------------- | --------- | ---------- |
| app\_book\_create | GET, POST | /books/new |

On your Twig template, these variables are available

| Name               | Type                                      |
| ------------------ | ----------------------------------------- |
| resource           | App\Entity\Book                           |
| book               | App\Entity\Book                           |
| operation          | Sylius\Resource\Metadata\Create           |
| resource\_metadata | Sylius\Resource\Metadata\ResourceMetadata |
| app                | Symfony\Bridge\Twig\AppVariable           |

The iterator for your books will be available as `books.data` or `resources.data`.

### Update operation

`Update` operation allows to edit an existing item of your resource.

{% tabs %}
{% tab title="PHP attributes" %}
{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Update;

#[AsResource(
    operations: [
        new Update(),
    ],
)
class Book implements ResourceInterface
{
}
```

{% endcode %}
{% endtab %}

{% tab title="External PHP file" %}
{% code title="config/sylius/resources/book.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

use App\Entity\Book;
use Sylius\Resource\Metadata\Operations;
use Sylius\Resource\Metadata\ResourceMetadata;
use Sylius\Resource\Metadata\Update;

return (new ResourceMetadata())
    ->withClass(Book::class)
    ->withOperations(
        new Operations([
            new Update(),
        ])    
    )
;
```

{% endcode %}
{% endtab %}
{% endtabs %}

It will configure this route for your `update` operation.

| Name              | Method          | Path             |
| ----------------- | --------------- | ---------------- |
| app\_book\_update | GET, PUT, PATCH | /books/{id}/edit |

On your Twig template, these variables are available

| Name               | Type                                      |
| ------------------ | ----------------------------------------- |
| resource           | App\Entity\Book                           |
| book               | App\Entity\Book                           |
| operation          | Sylius\Resource\Metadata\Update           |
| resource\_metadata | Sylius\Resource\Metadata\ResourceMetadata |
| app                | Symfony\Bridge\Twig\AppVariable           |

### Delete operation

`Delete` operation allows to remove an existing item of your resource.

{% tabs %}
{% tab title="PHP attributes" %}
{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Entity;

use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Delete;

#[AsResource(
    operations: [
        new Delete(),
    ],
)
class Book implements ResourceInterface
{
}
```

{% endcode %}
{% endtab %}

{% tab title="External PHP file" %}
{% code title="config/sylius/resources/book.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

use App\Entity\Book;
use Sylius\Resource\Metadata\Delete;
use Sylius\Resource\Metadata\Operations;
use Sylius\Resource\Metadata\ResourceMetadata;

return (new ResourceMetadata())
    ->withClass(Book::class)
    ->withOperations(
        new Operations([
            new Delete(),
        ])    
    )
;
```

{% endcode %}
{% endtab %}
{% endtabs %}

It will configure this route for your `delete` operation.

| Name              | Method | Path        |
| ----------------- | ------ | ----------- |
| app\_book\_delete | DELETE | /books/{id} |

### Bulk delete operation

`Bulk delete` operation allows to remove several items of your resource at the same time.

{% tabs %}
{% tab title="PHP attributes" %}
{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\BulkDelete;

#[AsResource(
    operations: [
        new BulkDelete(),
    ],
)
class Book implements ResourceInterface
{
}
```

{% endcode %}
{% endtab %}

{% tab title="External PHP file" %}
{% code title="config/sylius/resources/book.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

use App\Entity\Book;
use Sylius\Resource\Metadata\BulkDelete;
use Sylius\Resource\Metadata\Operations;
use Sylius\Resource\Metadata\ResourceMetadata;

return (new ResourceMetadata())
    ->withClass(Book::class)
    ->withOperations(
        new Operations([
            new BulkDelete(),
        ])
    )
;
```

{% endcode %}
{% endtab %}
{% endtabs %}

It will configure this route for your `bulk_delete` operation.

| Name                    | Method | Path                |
| ----------------------- | ------ | ------------------- |
| app\_book\_bulk\_delete | DELETE | /books/bulk\_delete |

### State machine operation

`State machine` operation allows to apply a transition to an item of your resource.

As an example, we add a `publish` operation to our book resource.

{% tabs %}
{% tab title="PHP attributes" %}
{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\ApplyStateMachineTransition;
use Sylius\Resource\Metadata\AsResource;

#[AsResource(
    operations: [
        new ApplyStateMachineTransition(stateMachineTransition: 'publish'),
    ],
)
class Book implements ResourceInterface
{
}
```

{% endcode %}
{% endtab %}

{% tab title="External PHP file" %}
{% code title="config/sylius/resources/book.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

use App\Entity\Book;
use Sylius\Resource\Metadata\ApplyStateMachineTransition;
use Sylius\Resource\Metadata\Operations;
use Sylius\Resource\Metadata\ResourceMetadata;

return (new ResourceMetadata())
    ->withClass(Book::class)
    ->withOperations(
        new Operations([
            new ApplyStateMachineTransition(stateMachineTransition: 'publish'),
        ])
    )
;
```

{% endcode %}
{% endtab %}
{% endtabs %}

It will configure this route for your `apply_state_machine_transition` operation.

| Name               | Method | Path                |
| ------------------ | ------ | ------------------- |
| app\_book\_publish | GET    | /books/{id}/publish |

## Advanced configuration

### Configure the path

It customizes the path for your operations.

{% code title="src/Entity/Customer.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Metadata\Update;

#[AsResource(
    operations: [
        new Create(path: 'register'),
        new Update(path: '{id}/edition'),
    ],
)
class Customer implements ResourceInterface
{
}
```

{% endcode %}

{% code title="config/sylius/resources/customer.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

use App\Entity\Customer;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Metadata\Operations;
use Sylius\Resource\Metadata\ResourceMetadata;
use Sylius\Resource\Metadata\Update;

return (new ResourceMetadata())
    ->withClass(Customer::class)
    ->withOperations(
        new Operations([
            new Create(path: 'register'),
            new Update(path: '{id}/edition'),
        ])
    )
;
```

{% endcode %}

| Name              | Method    | Path                |
| ----------------- | --------- | ------------------- |
| app\_book\_create | GET, POST | /books/register     |
| app\_book\_update | GET, POST | /books/{id}/edition |

### Configure the short name

It customizes the path for your operations.

{% code title="src/Entity/Customer.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Create;

#[AsResource(
    operations: [
        new Create(shortName: 'register'),
    ],
)
class Customer implements ResourceInterface
{
}
```

{% endcode %}

{% code title="config/sylius/resources/customer.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

use App\Entity\Customer;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Metadata\Operations;
use Sylius\Resource\Metadata\ResourceMetadata;

return (new ResourceMetadata())
    ->withClass(Customer::class)
    ->withOperations(
        new Operations([
            new Create(shortName: 'register'),
        ])
    )
;
```

{% endcode %}

| Name                | Method    | Path            |
| ------------------- | --------- | --------------- |
| app\_book\_register | GET, POST | /books/register |

It influences the path by default too, but you can still customize the path if needed.

### Configure the templates' dir

It defines the templates directory for your operations.

As an example, we defines `index`, `create`, `update` and `show` operations to our book resource.

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Metadata\Index;
use Sylius\Resource\Metadata\Show;
use Sylius\Resource\Metadata\Update;

#[AsResource(
    templatesDir: 'book',
    operations: [
        new Index(),
        new Create(),
        new Update(),
        new Show(),
    ],
)
class Book implements ResourceInterface
{
}
```

{% endcode %}

{% code title="config/sylius/resources/book.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

use App\Entity\Book;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Metadata\Index;
use Sylius\Resource\Metadata\Operations;
use Sylius\Resource\Metadata\ResourceMetadata;
use Sylius\Resource\Metadata\Show;
use Sylius\Resource\Metadata\Update;

return (new ResourceMetadata())
    ->withClass(Book::class)
    ->withTemplatesDir('book')
    ->withOperations(
        new Operations([
            new Index(),
            new Create(),
            new Update(),
            new Show(),
        ])
    )
;
```

{% endcode %}

| Operation | Template Path                    |
| --------- | -------------------------------- |
| index     | templates/books/index.html.twig  |
| create    | templates/books/create.html.twig |
| update    | templates/books/update.html.twig |
| show      | templates/books/show\.html.twig  |

{% hint style="info" %}
You can use `@SyliusAdminUi/crud` as templates dir from the [sylius/admin-ui](/admin-ui/getting-started) package.
{% endhint %}

| Operation | Template Path                        |
| --------- | ------------------------------------ |
| index     | @SyliusAdminUi/crud/index.html.twig  |
| create    | @SyliusAdminUi/crud/create.html.twig |
| update    | @SyliusAdminUi/crud/update.html.twig |
| show      | @SyliusAdminUi/crud/show\.html.twig  |

### Configure the routes' prefix

It adds a prefix to the path for each operation.

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\BulkDelete;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Metadata\Delete;
use Sylius\Resource\Metadata\Index;
use Sylius\Resource\Metadata\Show;
use Sylius\Resource\Metadata\Update;

#[AsResource(
    routePrefix: '/admin',
    operations: [
        new Index(routePrefix: ''),  // you can also customize the route prefix at the operation level too for extra flexibility
        new Create(),
        new Update(),
        new Delete(),
        new BulkDelete(),
        new Show(),
    ],
)
class Book implements ResourceInterface
{
}
```

{% endcode %}

{% code title="config/sylius/resources/book.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

use App\Entity\Book;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Metadata\Index;
use Sylius\Resource\Metadata\Operations;
use Sylius\Resource\Metadata\ResourceMetadata;
use Sylius\Resource\Metadata\Show;
use Sylius\Resource\Metadata\Update;

return (new ResourceMetadata())
    ->withClass(Book::class)
    ->withRoutePrefix('/admin')
    ->withOperations(
        new Operations([
            new Index(routePrefix: ''),  // you can also customize the route prefix at the operation level too for extra flexibility
            new Create(),
            new Update(),
            new Show(),
        ])
    )
;
```

{% endcode %}

| Name                    | Method          | Path                      |
| ----------------------- | --------------- | ------------------------- |
| app\_book\_index        | GET             | /books/                   |
| app\_book\_create       | GET, POST       | /admin/books/new          |
| app\_book\_update       | GET, PUT, PATCH | /admin/books/{id}/edit    |
| app\_book\_delete       | DELETE          | /admin/books/{id}         |
| app\_book\_bulk\_delete | DELETE          | /admin/books/bulk\_delete |
| app\_book\_show         | GET             | /admin/books/{id}         |

### Configure the routes' name

It customizes the route name for individual operations.

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\BulkDelete;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Metadata\Delete;
use Sylius\Resource\Metadata\Index;
use Sylius\Resource\Metadata\Show;
use Sylius\Resource\Metadata\Update;

#[AsResource(
    operations: [
        new Index(routeName: 'library_book_list'),
        new Create(routeName: 'library_book_add'),
        new Update(),
        new Delete(),
        new BulkDelete(),
        new Show(),
    ],
)
class Book implements ResourceInterface
{
}
```

{% endcode %}

{% code title="config/sylius/resources/book.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

use App\Entity\Book;
use Sylius\Resource\Metadata\BulkDelete;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Metadata\Delete;
use Sylius\Resource\Metadata\Index;
use Sylius\Resource\Metadata\Operations;
use Sylius\Resource\Metadata\ResourceMetadata;
use Sylius\Resource\Metadata\Show;
use Sylius\Resource\Metadata\Update;

return (new ResourceMetadata())
    ->withClass(Book::class)
    ->withOperations(
        new Operations([
            new Index(routeName: 'library_book_list'),
            new Create(routeName: 'library_book_add'),
            new Update(),
            new Delete(),
            new BulkDelete(),
            new Show(),            
        ])
    )
;
```

{% endcode %}

| Name                    | Method          | Path                |
| ----------------------- | --------------- | ------------------- |
| library\_book\_list     | GET             | /books/             |
| library\_book\_add      | GET, POST       | /books/new          |
| app\_book\_update       | GET, PUT, PATCH | /books/{id}/edit    |
| app\_book\_delete       | DELETE          | /books/{id}         |
| app\_book\_bulk\_delete | DELETE          | /books/bulk\_delete |
| app\_book\_show         | GET             | /books/{id}         |

### Configure the section

It changes the route name for each operation.

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\BulkDelete;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Metadata\Delete;
use Sylius\Resource\Metadata\Index;
use Sylius\Resource\Metadata\Show;
use Sylius\Resource\Metadata\Update;

#[AsResource(
    section: 'admin',
    routePrefix: '/admin',
    operations: [
        new Index(),
        new Create(),
        new Update(),
        new Delete(),
        new BulkDelete(),
    ],
)
#[AsResource(
    section: 'shop',
    operations: [
        new Index(),
        new Show(),
    ],
)
class Book implements ResourceInterface
{
}
```

{% endcode %}

{% code title="config/sylius/resources/admin/book.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

use App\Entity\Book;
use Sylius\Resource\Metadata\BulkDelete;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Metadata\Delete;
use Sylius\Resource\Metadata\Index;
use Sylius\Resource\Metadata\Operations;
use Sylius\Resource\Metadata\ResourceMetadata;
use Sylius\Resource\Metadata\Show;
use Sylius\Resource\Metadata\Update;

return (new ResourceMetadata())
    ->withClass(Book::class)
    ->withSection('admin')
    ->withRoutePrefix('/admin')
    ->withOperations(
        new Operations([
            new Index(),
            new Create(),
            new Update(),
            new Delete(),
            new BulkDelete(),
        ])
    )
;
```

{% endcode %}

{% code title="config/sylius/resources/shop/book.php" lineNumbers="true" %}

```php
use App\Entity\Book;
use Sylius\Resource\Metadata\BulkDelete;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Metadata\Delete;
use Sylius\Resource\Metadata\Index;
use Sylius\Resource\Metadata\Operations;
use Sylius\Resource\Metadata\ResourceMetadata;
use Sylius\Resource\Metadata\Show;
use Sylius\Resource\Metadata\Update;

return (new ResourceMetadata())
    ->withClass(Book::class)
    ->withSection('shop')
    ->withOperations(
        new Operations([
            new Index(),
            new Show(),
        ])
    )
;
```

{% endcode %}

| Name                           | Method          | Path                      |
| ------------------------------ | --------------- | ------------------------- |
| app\_admin\_book\_index        | GET             | /admin/books/             |
| app\_admin\_book\_create       | GET, POST       | /admin/books/new          |
| app\_admin\_book\_update       | GET, PUT, PATCH | /admin/books/{id}/edit    |
| app\_admin\_book\_delete       | DELETE          | /admin/books/{id}         |
| app\_admin\_book\_bulk\_delete | DELETE          | /admin/books/bulk\_delete |
| app\_shop\_book\_index         | GET             | /books/                   |
| app\_shop\_book\_show          | GET             | /books/{id}               |

### Configure the resource identifier

It changes the resource identifier for each operation.

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\BulkDelete;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Metadata\Delete;
use Sylius\Resource\Metadata\Index;
use Sylius\Resource\Metadata\Update;

#[AsResource(
    identifier: 'code',
    operations: [
        new Index(),
        new Create(),
        new Update(),
        new Delete(),
        new BulkDelete(),
    ],
)
class Book implements ResourceInterface
{
}
```

{% endcode %}

{% code title="config/sylius/resources/book.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

use App\Entity\Book;
use Sylius\Resource\Metadata\BulkDelete;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Metadata\Delete;
use Sylius\Resource\Metadata\Index;
use Sylius\Resource\Metadata\Operations;
use Sylius\Resource\Metadata\ResourceMetadata;
use Sylius\Resource\Metadata\Update;

return (new ResourceMetadata())
    ->withClass(Book::class)
    ->withIdentifier('code')
    ->withOperations(
        new Operations([
            new Index(),
            new Create(),
            new Update(),
            new Delete(),
            new BulkDelete(),          
        ])
    )
;
```

{% endcode %}

| Name                    | Method          | Path                      |
| ----------------------- | --------------- | ------------------------- |
| app\_book\_index        | GET             | /admin/books/             |
| app\_book\_create       | GET, POST       | /admin/books/new          |
| app\_book\_update       | GET, PUT, PATCH | /admin/books/{code}/edit  |
| app\_book\_delete       | DELETE          | /admin/books/{code}       |
| app\_book\_bulk\_delete | DELETE          | /admin/books/bulk\_delete |

### Configure the vars

It defines the simple vars that you can use on your templates.

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Create;

#[AsResource(
    vars: [
        'header' => 'Library', 
        'subheader' => 'Managing your library',
    ],
    operations: [
        new Create(vars: [
            'subheader' => 'Adding a book',
        ]),
    ],
)
class Book implements ResourceInterface
{
}
```

{% endcode %}

{% code title="config/sylius/resources/book.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

use App\Entity\Book;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Metadata\Operations;
use Sylius\Resource\Metadata\ResourceMetadata;

return (new ResourceMetadata())
    ->withClass(Book::class)
    ->withVars([
        'header' => 'Library', 
        'subheader' => 'Managing your library',
    ])
    ->withOperations(
        new Operations([
            new Create(vars: [
                'subheader' => 'Adding a book',
            ]),         
        ])
    )
;
```

{% endcode %}

You can use these vars on your Twig templates. These vars will be available on any operations for this resource.

```html
<h1>{{ operation.vars.header }}<!-- Library --></h1>
<h2>{{ operation.vars.subheader }}<!-- Adding a book --></h2>
```


# Validation

* [on HTML request](#on-html-request)
* [on API request](#on-api-request)
* [Disable validation](#disable-validation)

It uses `symfony/validator` to validate your data.

## on HTML request

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
namespace App\Entity;

use App\Form\Type\BookType;
use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Create;
use Symfony\Component\Validator\Constraints as Assert;

#[AsResource(
    formType: BookType::class, 
    operations: [
        new Create(),
    ],
)
class Book implements ResourceInterface
{
    // ...
    #[Assert\NotBlank()]
    private ?string $title;
}
```

{% endcode %}

In this example, validation will fail when adding a new book without specifying its title on the form.

## on API request

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
namespace App\Entity;

use App\Form\Type\BookType;
use Sylius\Resource\Metadata\Api\Post;
use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Symfony\Component\Validator\Constraints as Assert;

#[AsResource(
    operations: [
        new Post(),
    ],
)
class Book implements ResourceInterface
{
    // ...
    #[Assert\NotBlank()]
    private ?string $title;
}
```

{% endcode %}

In this example, validation will fail when adding a new book without specifying its title on the payload.

## Disable validation

In some case, you may want to disable this validation.

For example, in a "publish" operation, you may want to apply a state machine transition without validation existing data.

{% code title="src/BoardGameBlog/Infrastructure/Sylius/Resource/BoardGameResource.php" lineNumbers="true" %}

```php
namespace App\BoardGameBlog\Infrastructure\Sylius\Resource;

use App\BoardGameBlog\Infrastructure\Sylius\State\Http\Processor\PublishBoardGameProcessor;
use App\BoardGameBlog\Infrastructure\Sylius\State\Http\Provider\BoardGameItemProvider;
use App\BoardGameBlog\Infrastructure\Symfony\Form\Type\BoardGameType;
use Sylius\Resource\Metadata\ApplyStateMachineTransition;
use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Create;
use Symfony\Component\Validator\Constraints as Assert;

#[AsResource(
    formType: BoardGameType::class, 
    operations: [
        new Update(
            provider: BoardGameItemProvider::class, 
            processor: PublishBoardGameProcessor::class,
            validate: false, // disable resource validation        
        ),
    ],
)
class BoardGameResource implements ResourceInterface
{
}
```

{% endcode %}


# Redirect

After that an action has been performed, the operation can be redirected to another operation.

* [Default redirections](#default-redirections)
* [Custom redirection](#custom-redirection)
* [Pass arguments to your redirection](#pass-arguments-to-your-redirection)

## Default redirections

Redirections are configured on your operations with these default behaviours.

| Operation    | Redirection                         |
| ------------ | ----------------------------------- |
| create       | `show` if exists, otherwise `index` |
| update       | `show` if exists, otherwise `index` |
| delete       | `index`                             |
| bulk\_delete | `index`                             |

## Custom redirection

For example, let's configure a custom redirection to create & update operations.

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\Entity;

use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Metadata\Update;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    operations: [
        new Create(
            redirectToRoute: 'app_book_update',
        ),
        new Update(
            redirectToRoute: 'app_book_update',
        ),
    ],
)
class Book implements ResourceInterface
{
}
```

{% endcode %}

After adding or editing a book, it will be redirected to the edition page of a book.

## Pass arguments to your redirection

You can pass arguments to your redirection method.

3 variables are available:

* `resource`: to retrieve data from the instantiated resource
* `{name_of_your_resource}`: If your resource is a book instance, it will be also available as `book` variable
* `request`: to retrieve data from the request via Symfony\Component\HttpFoundation\Request

It uses the [Symfony expression language](https://symfony.com/doc/current/components/expression_language.html) component.

As an example, let's redirect a book creation to the author details page of the created book.

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\Entity;

use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    operations: [
        new Create(
            redirectToRoute: 'app_author_show', 
            # You can use either the generic resource variable
            redirectArguments: ['id' => 'resource.getAuthor().getId()']
            # Or you can use the resource name
            redirectArguments: ['id' => 'book.getAuthor().getId()']
        ),
    ],
)
class Book implements ResourceInterface
{
}
```

{% endcode %}


# Resource factories

Resource factories are used on Create operations to instantiate your resource.

* [Resource factories](#resource-factories)
  * [Default factory for your resource](#default-factory-for-your-resource)
  * [Inject the factory in your service](#inject-the-factory-in-your-service)
  * [Define your custom factory](#define-your-custom-factory)
  * [Use your custom method](#use-your-custom-method)
  * [Pass arguments to your method](#pass-arguments-to-your-method)
  * [Use a factory without declaring it](#use-a-factory-without-declaring-it-)
  * [Use a callable for your custom factory](#use-a-callable-for-your-custom-factory)

## Default factory for your resource

By default, a resource factory is defined to your resource `Sylius\Component\Resource\Factory\Factory`.

It has a `createNew` method with no arguments.

## Inject the factory in your service

If you are using Symfony autowiring, you can inject the resource factory using the right variable name.

{% code title="src/MyService.php" lineNumbers="true" %}

```php
namespace App;

use Sylius\Resource\Factory\FactoryInterface;

final class MyService
{
    public function __construct(
        private FactoryInterface $bookFactory,
    ) {}
}
```

{% endcode %}

In this example, the `app.factory.book` will be injected in your `$bookFactory`

You can find the variable name using this debug command:

```shell
$ bin/console debug:autowiring app.factory.book
```

## Define your custom factory

{% code title="src/Factory/BookFactory.php" lineNumbers="true" %}

```php
declare(strict_types=1);

namespace App\Factory;

use App\Entity\Book;
use Sylius\Resource\Factory\FactoryInterface;

final class BookFactory implements FactoryInterface
{
    public function createNew(): Book
    {
        $book = new Book();
        $book->setCreatedAt(new \DateTimeImmutable());
        
        return $book;
    }
}
```

{% endcode %}

Configure your factory

{% code title="config/services.yaml" lineNumbers="true" %}

```yaml
services:
    App\Factory\BookFactory:
        decorates: 'app.factory.book'
```

{% endcode %}

## Use your custom method

{% code title="src/Factory/BookFactory.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\Factory;

use App\Entity\Book;
use Sylius\Resource\Factory\FactoryInterface;
use Symfony\Component\Security\Core\Security;

final class BookFactory implements FactoryInterface
{
    public function __construct(private Security $security) 
    {
    }

    public function createNew(): Book
    {
        return new Book();
    }
    
    public function createWithCreator(): Book
    {
        $book = $this->createNew();
        
        $book->setCreator($this->security->getUser());
        
        return $book;
    }
}
```

{% endcode %}

Use it on your create operation

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\Entity\Book;

use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    operations: [
        new Create(
            path: 'authors/{authorId}/books',
            factoryMethod: 'createWithCreator',
        ),
    ],
)
class Book implements ResourceInterface
{
}
```

{% endcode %}

## Pass arguments to your method

You can pass arguments to your factory method.

3 variables are available:

* `request`: to retrieve data from the request via `Symfony\Component\HttpFoundation\Request`
* `token`: to retrieve data from the authentication token via `Symfony\Component\Security\Core\Authentication\Token\TokenInterface`
* `user`: to retrieve data from the logged-in user via `Symfony\Component\Security\Core\User\UserInterface`

It uses the [Symfony expression language](https://symfony.com/doc/current/components/expression_language.html) component.

{% code title="src/Factory/BookFactory.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\Factory;

use App\Entity\Book;
use Sylius\Resource\Doctrine\Persistence\RepositoryInterface;
use Sylius\Resource\Factory\FactoryInterface;

final class BookFactory implements FactoryInterface
{
    public function __construct(private RepositoryInterface $authorRepository) 
    {
    }

    public function createNew(): Book
    {
        return new Book();
    }
    
    public function createForAuthor(string $authorId): Book
    {
        $book = $this->createNew();
        
        $author = $this->authorRepository->find($authorId);
        
        $book->setAuthor($author);
        
        return $book;
    }
}
```

{% endcode %}

Use it on your create operation

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\Entity\Book;

use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Create;

#[AsResource(
    operations: [
        new Create(
            path: 'authors/{authorId}/books',
            factoryMethod: 'createForAuthor',
            factoryArguments: ['authorId' => "request.attributes.get('authorId')"],
        ),
    ],
)
class Book implements ResourceInterface
{
}
```

{% endcode %}

## Use a factory without declaring it

You can use a factory without declaring it on `services.yaml`.

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\Entity\Book;

use App\Factory\BookFactory;
use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Create;

#[AsResource(
    operations: [
        new Create(
            path: 'authors/{authorId}/books',
            # Here we declared the factory to use with its fully classified class name
            factory: BookFactory::class,
            factoryMethod: 'createForAuthor', 
            factoryArguments: ['authorId' => "request.attributes.get('authorId')"],
        ),
    ],
)
class Book implements ResourceInterface
{
}
```

{% endcode %}

## Use a callable for your custom factory

{% code title="src/Factory/BookFactory.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\Factory;

use App\Entity\Book;

final class BookFactory
{    
    public static function create(): Book
    {
        return new Book();
    }
}
```

{% endcode %}

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\Entity\Book;

use App\Factory\BookFactory;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Model\ResourceInterface

#[AsResource(
    operations: [
        new Create(
            factory: [BookFactory::class, 'create'], 
        ),
    ],
)
class Book implements ResourceInterface
{
}
```

{% endcode %}


# Providers

Providers retrieve data from your persistence layer.

* [Default providers](#default-providers)
  * [Custom repository method](#custom-repository-method)
  * [Custom repository arguments](#custom-repository-arguments)
* [Custom providers](#custom-providers)
* [Disable providing data](#disable-providing-data)

## Default providers

When your resource is a Doctrine entity, there's a default provider `Sylius\Component\Resource\Symfony\Request\State\Provider` which is already configured to your operations.

As it uses the Doctrine repository configured on your resource, some default repository methods are used.

| Operation   | Repository method |
| ----------- | ----------------- |
| index       | createPaginator   |
| show        | findOneBy         |
| update      | findOneBy         |
| delete      | findOneBy         |
| bulk delete | findById          |

### Custom repository method

You can customize the method to use.

{% code title="src/Entity/Customer.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\Entity;

use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Show;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    operations: [
        new Show(
            repositoryMethod: 'findOneByEmail',
        ),
    ],
)
final class Customer implements ResourceInterface
{
    // [...]
}
```

{% endcode %}

### Custom repository arguments

You can pass arguments to your repository method.

3 variables are available:

* `request`: to retrieve data from the request via `Symfony\Component\HttpFoundation\Request`
* `token`: to retrieve data from the authentication token via `Symfony\Component\Security\Core\Authentication\Token\TokenInterface`
* `user`: to retrieve data from the logged-in user via `Symfony\Component\Security\Core\User\UserInterface`

It uses the [Symfony expression language](https://symfony.com/doc/current/components/expression_language.html) component.

{% code title="src/Entity/Customer.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\Entity;

use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Show;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    operations: [
        new Show(
            repositoryMethod: 'findOneByEmail', 
            repositoryArguments: ['email' => "request.attributes.get('email')"],
        ),
    ],
)
final class Customer implements ResourceInterface
{
    // [...]
}
```

{% endcode %}

## Custom providers

Custom providers are useful to customize your logic to retrieve data and for an advanced usage such as an hexagonal architecture.

As an example, let's configure a `BoardGameItemProvider` on a `BoardGameResource` which is not a Doctrine entity.

{% code title="src/BoardGameBlog/Infrastructure/Sylius/State/Provider/BoardGameItemProvider.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\BoardGameBlog\Infrastructure\Sylius\State\Provider;

use Sylius\Resource\State\ProviderInterface;

final class BoardGameItemProvider implements ProviderInterface
{
    public function __construct(
        private QueryBusInterface $queryBus,
    ) {
    }

    public function provide(Operation $operation, Context $context): object|array|null
    {
        $request = $context->get(RequestOption::class)?->request();
        Assert::notNull($request);

        $id = (string) $request->attributes->get('id');

        $model = $this->queryBus->ask(new FindBoardGameQuery(new BoardGameId(Uuid::fromString($id))));

        return null !== $model ? BoardGameResource::fromModel($model) : null;
    }
}
```

{% endcode %}

Use this provider on your operation.

{% code title="src/BoardGameBlog/Infrastructure/Sylius/Resource/BoardGameResource.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\BoardGameBlog\Infrastructure\Sylius\Resource;

use App\BoardGameBlog\Infrastructure\Sylius\State\Provider\BoardGameItemProvider;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Show;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    operations: [
        new Show(
            provider: BoardGameItemProvider::class, 
        ),
    ],
)
final class BoardGameResource implements ResourceInterface
{
    // [...]
}
```

{% endcode %}

## Disable providing data

In some cases, you may want not to read data.

For example, in a delete operation, you can implement your custom delete processor without reading it before.

{% code title="src/BoardGameBlog/Infrastructure/Sylius/Resource/BoardGameResource.php" lineNumbers="true" %}

```php

declare(strict_types=1);

namespace App\BoardGameBlog\Infrastructure\Sylius\Resource;

use App\BoardgameBlog\Infrastructure\Sylius\State\Provider\DeleteBoardGameProcessor;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Delete;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    operations: [
        new Delete(
            processor: DeleteBoardGameProcessor::class,
            read: false,
        ),
    ],
)
final class BoardGameResource implements ResourceInterface
{
    // [...]
}
```

{% endcode %}

{% code title="src/BoardGameBlog/Infrastructure/Sylius/State/Processor/DeleteBoardGameProcessor.php" lineNumbers="true" %}

```php

namespace App\BoardGameBlog\Infrastructure\Sylius\State\Processor;

use App\BoardGameBlog\Application\Command\DeleteBoardGameCommand;
use App\BoardGameBlog\Domain\ValueObject\BoardGameId;
use App\BoardGameBlog\Infrastructure\Sylius\Resource\BoardGameResource;
use App\Shared\Application\Command\CommandBusInterface;
use Sylius\Resource\Context\Context;
use Sylius\Resource\Context\Option\RequestOption;
use Sylius\Resource\Metadata\Operation;
use Sylius\Resource\State\ProcessorInterface;
use Webmozart\Assert\Assert;

final class DeleteBoardGameProcessor implements ProcessorInterface
{
    public function __construct(
        private CommandBusInterface $commandBus,
    ) {
    }

    public function process(mixed $data, Operation $operation, Context $context): mixed
    {
        Assert::isInstanceOf($data, BoardGameResource::class);
        
        // Data is not provided in this case, so you will need to get it from the HTTP request
        $id = $context->get(RequestOption::class)?->attributes->get('id') ?? null;
        Assert::notNull($id);

        $this->commandBus->dispatch(new DeleteBoardGameCommand(new BoardGameId($id)));

        return null;
    }
}
```

{% endcode %}


# Processors

Processors process data: send an email, persist to storage, add to queue etc.

* [Default processors](#default-processors)
* [Custom processors](#custom-processors)
  * [Example #1: Sending an email after persisting data](#example-1-sending-an-email-after-persisting-data)
  * [Example #2: Use a custom delete processor](#example-2-use-a-custom-delete-processor)
* [Disable processing data](#disable-processing-data)

## Default processors

When your resource is a Doctrine entity, there are default processors which are already configured to your operations.

As it uses the Doctrine repository configured on your resource, it will automatically flush data for you.

| Operation   | Processor                                              |
| ----------- | ------------------------------------------------------ |
| create      | Sylius\Resource\Doctrine\Common\State\PersistProcessor |
| update      | Sylius\Resource\Doctrine\Common\State\PersistProcessor |
| delete      | Sylius\Resource\Doctrine\Common\State\RemoveProcessor  |
| bulk delete | Sylius\Resource\Doctrine\Common\State\RemoveProcessor  |

## Custom processors

Custom processors are useful to customize your logic to send an email, persist data to storage, add to queue and for an advanced usage such as an hexagonal architecture.

### Example #1: Sending an email after persisting data

As an example, send an email after customer registration

{% code title="src/Sylius/State/Processor/CreateCustomerProcessor.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Sylius\State\Processor;

// ...
use Sylius\Resource\Context\Context;
use Sylius\Component\Customer\Model\CustomerInterface;
use Sylius\Resource\Doctrine\Common\State\PersistProcessor;
use Sylius\Resource\Metadata\Operation;
use Sylius\Resource\State\ProcessorInterface;

final class CreateCustomerProcessor implements ProcessorInterface
{
    public function __construct(
        private CommandBusInterface $commandBus,
        private PersistProcessor $decorated,
    ) {
    }

    public function process(mixed $data, Operation $operation, Context $context): mixed
    {
        Assert::isInstanceOf($data, Customer::class);
        
        $this->decorated->process($data, $operation, $context);

        // Here your logic to send a registration email.
        $this->commandBus->dispatch(new SendRegistrationEmailCommand(new CustomerId($data->id)));

        return null;
    }
}
```

{% endcode %}

Use this processor on your operation.

{% code title="src/Entity/Customer.php" lineNumbers="true" %}

```php

namespace App\Entity\Customer;

use App\Sylius\State\Processor\CreateCustomerProcessor;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    operations: [
        new Create(
            processor: CreateCustomerProcessor::class,
        ),
    ],
)
final class BoardGameResource implements ResourceInterface
```

{% endcode %}

### Example #2: Use a custom delete processor

As another example, let's configure a `DeleteBoardGameProcessor` on a `BoardGameResource` which is not a Doctrine entity.

{% code title="src/BoardGameBlog/Infrastructure/Sylius/State/Processor/DeleteBoardGameProcessor.php" lineNumbers="true" %}

```php

namespace App\BoardGameBlog\Infrastructure\Sylius\State\Processor;

final class DeleteBoardGameProcessor implements ProcessorInterface
{
    public function __construct(
        private CommandBusInterface $commandBus,
    ) {
    }

    public function process(mixed $data, Operation $operation, Context $context): mixed
    {
        Assert::isInstanceOf($data, BoardGameResource::class);

        $this->commandBus->dispatch(new DeleteBoardGameCommand(new BoardGameId($data->id)));

        return null;
    }
}
```

{% endcode %}

Use this processor on your operation.

{% code title="src/BoardGameBlog/Infrastructure/Sylius/Resource/BoardGameResource.php" lineNumbers="true" %}

```php

namespace App\BoardGameBlog\Infrastructure\Sylius\Resource;

use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Delete;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    section: 'admin',
    formType: BoardGameType::class,
    templatesDir: 'crud',
    routePrefix: '/admin',
    operations: [
        new Delete(
            processor: DeleteBoardGameProcessor::class,
        ),
    ],
)
final class BoardGameResource implements ResourceInterface
```

{% endcode %}

Note that in a delete operation, you can disable providing data.\
See [Disable providing data](/resource/index/providers#disable-providing-data) chapter.

## Disable processing data

In some cases, you may want not to write data.

For example, you can implement a preview for the updated data without saving them into your storage.

{% code title="src/BoardGameBlog/Infrastructure/Sylius/Resource/BoardGameResource.php" lineNumbers="true" %}

```php

namespace App\BoardGameBlog\Infrastructure\Sylius\Resource;

use App\BoardGameBlog\Infrastructure\Sylius\State\Http\Provider\BoardGameItemProvider;
use App\BoardGameBlog\Infrastructure\Symfony\Form\Type\BoardGameType;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Update;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    section: 'admin',
    formType: BoardGameType::class,
    templatesDir: 'crud',
    routePrefix: '/admin',
    operations: [
        new Update(
            shortName: 'update_preview',
            provider: BoardGameItemProvider::class,
            write: false,   
        ),
    ],
)]
final class BoardGameResource implements ResourceInterface
```

{% endcode %}


# Responders

Responders respond data: transform data to a Symfony response, return a success in a CLI operation.

* [Default responders](#default-responders)
* [Twig Responder](#twig-responder)
  * [Customize Twig template variables](#customize-twig-template-variables)
* [API Responder](#api-responder)

## Default responders

When your operation is an instance of `Sylius\Component\Resource\Metadata\HttpOperation` two responders are configured by default.

The responder will automatically choose the responder depending on the request format:

| Request format | Responder                                                     |
| -------------- | ------------------------------------------------------------- |
| html           | Sylius\Component\Resource\Symfony\Request\State\TwigResponder |
| json           | Sylius\Component\Resource\Symfony\Request\State\ApiResponder  |
| xml            | Sylius\Component\Resource\Doctrine\Common\State\ApiResponder  |

## Twig Responder

The Twig responder is used to render data into a Symfony response.\
It's used for HTML responses.

The variables that are passed to the Twig templates depends on the operation (See [Configure your operations](/resource/index/configure_your_operations) chapter).

### Customize Twig template variables

Some variables are already available on your operations, but you can add more variables easily.

As an example, we add a `foo` variable to the Twig template with `bar` as value.

{% code title="src/Twig/Context/Factory/ShowSubscriptionContextFactory.php" lineNumbers="true" %}

```php

namespace App\Twig\Context\Factory;

use Sylius\Resource\Context\Context;
use Sylius\Resource\Metadata\Operation;
use Sylius\Resource\Twig\Context\Factory\ContextFactoryInterface;

final class ShowSubscriptionContextFactory implements ContextFactoryInterface
{
    public function __construct(private ContextFactoryInterface $decorated)
    {
    }

    public function create(mixed $data, Operation $operation, Context $context): array
    {
        return array_merge($this->decorated->create($data, $operation, $context), [
            'foo' => 'bar',
        ]);
    }
}
```

{% endcode %}

Use it on your operation.

{% code title="src/Entity/Subscription.php" lineNumbers="true" %}

```php

namespace App\Entity;

use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Show;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    operations: [
        new Show(
            template: 'subscription/show.html.twig',
            twigContextFactory: ShowSubscriptionContextFactory::class,      
        ),
    ],
)
class Subscription implements ResourceInterface
{
}
```

{% endcode %}

## API Responder

The API responder is used to render serialized data into a Symfony response.\
It's used for JSON/XML responses.


# Legacy Resource Documentation

Legacy Resource Documentation

{% hint style="warning" %}
This section is deprecated. However, as of now, the Sylius E-Commerce project is still resorting to this configuration so you might want to check it out.
{% endhint %}

* [Configuring Your Resources](/resource/index/index/configuration)
* [Services](/resource/index/index/services)
* [Routing](/resource/index/index/routing)
* [Forms](/resource/index/index/forms)
* [Getting a Single Resource](/resource/index/index/show_resource)
* [Getting a Collection of Resources](/resource/index/index/index_resources)
* [Creating Resources](/resource/index/index/create_resource)
* [Updating Resources](/resource/index/index/update_resource)
* [Deleting Resources](/resource/index/index/delete_resource)
* [Configuring a state machine](/resource/index/index/state_machine)
* [Configuration Reference](/resource/index/index/reference)


# Configuration

{% hint style="warning" %}
This section is deprecated. However, as of now, the Sylius E-Commerce project is still resorting to this configuration so you might want to check it out.
{% endhint %}

Now you need to configure your first resource. Let's assume you have a *Book* entity in your application and it has simple fields:

* id
* title
* author
* description

You can see a full exemplary configuration of a typical resource [How to add a custom model?](https://docs.sylius.com/en/latest/cookbook/entities/custom-model.html)

## Implement the ResourceInterface in your model class.

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Model\ResourceInterface;

class Book implements ResourceInterface
{
    // Most of the time you have the code below already in your class.
    protected $id;

    public function getId()
    {
        return $this->id;
    }
}
```

{% endcode %}

## Configure the class as a resource.

In your `config/packages/sylius_resource.yaml` add:

{% code title="config/packages/sylius\_resource.yaml" lineNumbers="true" %}

```yaml
sylius_resource:
    resources:
        app.book:
            classes:
                model: App\Entity\Book
```

{% endcode %}

That's it! Your Book entity is now registered as Sylius Resource.

## You can also configure several doctrine drivers.

Remember that the `doctrine/orm` driver is used by default.

{% code title="config/packages/sylius\_resource.yaml" lineNumbers="true" %}

```yaml
sylius_resource:
    drivers:
        - doctrine/orm
        - doctrine/phpcr-odm
    resources:
        app.book:
            classes:
                model: App\Entity\Book
        app.article:
            driver: doctrine/phpcr-odm
            classes:
                model: App\Document\ArticleDocument
```

{% endcode %}

## Update the resource repository

If you use the "make:entity" command you should have a generated repository which extends ServiceEntityRepository. Then you just have to implement `SyliusRepositoryInterface` and use `ResourceRepositoryTrait`.

{% code title="src/Repository/BookRepository.php" lineNumbers="true" %}

```php
namespace App\Repository;

use App\Entity\Book;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Sylius\Resource\Doctrine\Persistence\RepositoryInterface;

class BookRepository extends ServiceEntityRepository implements RepositoryInterface
{
    use ResourceRepositoryTrait;

    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Book::class);
    }
}
```

{% endcode %}

And configure this repository class:

{% code title="config/packages/sylius\_resource.yaml" lineNumbers="true" %}

```yaml
sylius_resource:
    drivers:
        - doctrine/orm
        - doctrine/phpcr-odm
    resources:
        app.book:
            classes:
                model: App\Entity\Book
                repository: App\Entity\BookRepository
```

{% endcode %}

## Generate API routing.

Learn more about using Sylius REST API in these articles: [How to use Sylius API? - Cookbook](https://docs.sylius.com/en/latest/cookbook/api/api.html)

Add the following lines to `config/routes.yaml`:

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book:
    resource: |
        alias: app.book
    type: sylius.resource_api
```

{% endcode %}

After that a full JSON/XML CRUD API is ready to use. Sounds crazy? Spin up the built-in server and give it a try:

```bash
php bin/console server:run
```

You should see something like:

```bash
Server running on http://127.0.0.1:8000

Quit the server with CONTROL-C.
```

Now, in a separate Terminal window, call these commands:

```bash
curl -i -X POST -H "Content-Type: application/json" -d '{"title": "Lord of The Rings", "author": "J. R. R. Tolkien", "description": "Amazing!"}' http://localhost:8000/books/
curl -i -X GET -H "Accept: application/json" http://localhost:8000/books/
```

As you can guess, other CRUD actions are available through this API.

## Generate web routing.

What if you want to render HTML pages? That's easy! Update the routing configuration:

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book:
    resource: |
        alias: app.book
    type: sylius.resource
```

{% endcode %}

This will generate routing for HTML views.

Run the `debug:router` command to see available routes:

```bash
php bin/console debug:router
```

```
------------------------ --------------- -------- ------ -------------------------
Name                     Method          Scheme   Host   Path
------------------------ --------------- -------- ------ -------------------------
app_book_show            GET             ANY      ANY    /books/{id}
app_book_index           GET             ANY      ANY    /books/
app_book_create          GET|POST        ANY      ANY    /books/new
app_book_update          GET|PUT|PATCH   ANY      ANY    /books/{id}/edit
app_book_delete          DELETE          ANY      ANY    /books/{id}
```

Do you need **views** for your newly created entity? Read more about [Grids](https://docs.sylius.com/en/latest/components_and_bundles/bundles/SyliusGridBundle/index.html), which are a separate bundle of Sylius, but may be very useful for views generation.

##

You can configure more options for the routing generation but you can also define each route manually to have it fully configurable. Continue reading to learn more!

[**Go back to the documentation's index**](/resource/index/index)


# Services

{% hint style="warning" %}
This section is deprecated. However, as of now, the Sylius E-Commerce project is still resorting to this configuration so you might want to check it out.
{% endhint %}

When you register an entity as a resource, several services are registered for you. For the `app.book` resource, the following services are available:

* `app.controller.book` - instance of `ResourceController`;
* `app.factory.book` - instance of [FactoryInterface](https://docs.sylius.com/en/latest/components_and_bundles/components/Resource/factory.html#component-resource-factory-factory-interface);
* `app.repository.book` - instance of [RepositoryInterface](https://docs.sylius.com/en/latest/components_and_bundles/components/Resource/repository.html#component-resource-repository-repository-interface);
* `app.manager.book` - alias to an appropriate Doctrine's `ObjectManager`.

[**Go back to the documentation's index**](/resource/index/index)


# Routing

{% hint style="warning" %}
This section is deprecated. However, as of now, the Sylius E-Commerce project is still resorting to this configuration so you might want to check it out.
{% endhint %}

SyliusResourceBundle ships with a custom route loader that can save you some time.

## Generating Generic CRUD Routing

To generate a full CRUD routing, simply configure it in your `config/routes.yaml`:

<details>

<summary>Yaml</summary>

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book:
    resource: |
        alias: app.book
    type: sylius.resource
```

{% endcode %}

</details>

<details>

<summary>PHP</summary>

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
use Sylius\Resource\Annotation\SyliusCrudRoutes;

#[SyliusCrudRoutes(
    alias: 'app.book',
)]
```

{% endcode %}

</details>

Results in the following routes:

```bash
php bin/console debug:router
```

```
------------------------ --------------- -------- ------ -------------------------
Name                     Method          Scheme   Host   Path
------------------------ --------------- -------- ------ -------------------------
app_book_index           GET             ANY      ANY    /books/
app_book_create          GET|POST        ANY      ANY    /books/new
app_book_update          GET|PUT|PATCH   ANY      ANY    /books/{id}/edit
app_book_show            GET             ANY      ANY    /books/{id}
app_book_bulk_delete     DELETE          ANY      ANY    /books/bulk-delete
app_book_delete          DELETE          ANY      ANY    /books/{id}
```

## Using a Custom Path

By default, Sylius will use a plural form of the resource name, but you can easily customize the path:

<details>

<summary>Yaml</summary>

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book:
    resource: |
        alias: app.book
        path: library
    type: sylius.resource
```

{% endcode %}

</details>

<details>

<summary>PHP</summary>

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
use Sylius\Resource\Annotation\SyliusCrudRoutes;

#[SyliusCrudRoutes(
    alias: 'app.book',
    path: 'library',
)]
```

{% endcode %}

</details>

Results in the following routes:

```bash
php bin/console debug:router
```

```
------------------------ --------------- -------- ------ -------------------------
Name                     Method          Scheme   Host   Path
------------------------ --------------- -------- ------ -------------------------
app_book_index           GET             ANY      ANY    /library/
app_book_create          GET|POST        ANY      ANY    /library/new
app_book_update          GET|PUT|PATCH   ANY      ANY    /library/{id}/edit
app_book_show            GET             ANY      ANY    /library/{id}
app_book_bulk_delete     DELETE          ANY      ANY    /library/bulk-delete
app_book_delete          DELETE          ANY      ANY    /library/{id}
```

## Generating API CRUD Routing

To generate a full API-friendly CRUD routing, add these YAML lines to your `config/routes.yaml`:

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book:
    resource: |
        alias: app.book
    type: sylius.resource_api
```

{% endcode %}

Results in the following routes:

```bash
php bin/console debug:router
```

```
------------------------ --------------- -------- ------ -------------------------
Name                     Method          Scheme   Host   Path
------------------------ --------------- -------- ------ -------------------------
app_book_show            GET             ANY      ANY    /books/{id}
app_book_index           GET             ANY      ANY    /books/
app_book_create          POST            ANY      ANY    /books/
app_book_update          PUT|PATCH       ANY      ANY    /books/{id}
app_book_delete          DELETE          ANY      ANY    /books/{id}
```

## Excluding Routes

If you want to skip some routes, simply use `except` configuration:

<details>

<summary>Yaml</summary>

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book:
    resource: |
        alias: app.book
        except: ['delete', 'update']
    type: sylius.resource
```

{% endcode %}

</details>

<details>

<summary>PHP</summary>

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
use Sylius\Resource\Annotation\SyliusCrudRoutes;

#[SyliusCrudRoutes(
    alias: 'app.book',
    except: ['delete', 'update'],
)]
```

{% endcode %}

</details>

Results in the following routes:

```bash
php bin/console debug:router
```

```
------------------------ --------------- -------- ------ -------------------------
Name                     Method          Scheme   Host   Path
------------------------ --------------- -------- ------ -------------------------
app_book_index           GET             ANY      ANY    /books/
app_book_create          GET|POST        ANY      ANY    /books/new
app_book_show            GET             ANY      ANY    /books/{id}
app_book_bulk_delete     DELETE          ANY      ANY    /books/bulk-delete
```

## Generating Only Specific Routes

If you want to generate only some specific routes, simply use the `only` configuration:

<details>

<summary>Yaml</summary>

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book:
    resource: |
        alias: app.book
        only: ['show', 'index']
    type: sylius.resource
```

{% endcode %}

</details>

<details>

<summary>PHP</summary>

{% code title="src/Entity/Book" lineNumbers="true" %}

```php
use Sylius\Resource\Annotation\SyliusCrudRoutes;

#[SyliusCrudRoutes(
    alias: 'app.book',
    only: ['show', 'index'],
)]
```

{% endcode %}

</details>

Results in the following routes:

```bash
php bin/console debug:router
```

```
------------------------ --------------- -------- ------ -------------------------
Name                     Method          Scheme   Host   Path
------------------------ --------------- -------- ------ -------------------------
app_book_index           GET             ANY      ANY    /books/
app_book_show            GET             ANY      ANY    /books/{id}
```

## Generating Routing for a Section

Sometimes you want to generate routing for different "sections" of an application:

<details>

<summary>Yaml</summary>

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_admin_book:
    resource: |
        alias: app.book
        section: admin
    type: sylius.resource
    prefix: /admin

app_library_book:
    resource: |
        alias: app.book
        section: library
        only: ['show', 'index']
    type: sylius.resource
    prefix: /library
```

{% endcode %}

The generation results in the following routes:

```bash
php bin/console debug:router
```

```
-------------------------- --------------- -------- ------ -------------------------
Name                        Method          Scheme   Host   Path
-------------------------- --------------- -------- ------ -------------------------
app_admin_book_index        GET             ANY      ANY    /admin/books/
app_admin_book_create       GET|POST        ANY      ANY    /admin/books/new
app_admin_book_update       GET|PUT|PATCH   ANY      ANY    /admin/books/{id}/edit
app_admin_book_show         GET             ANY      ANY    /admin/books/{id}
app_admin_book_bulk_delete  DELETE          ANY      ANY    /admin/books/bulk-delete
app_admin_book_delete       DELETE          ANY      ANY    /admin/books/{id}
app_library_book_show       GET             ANY      ANY    /library/books/{id}
app_library_book_index      GET             ANY      ANY    /library/books/
```

### Using Custom Templates

By default, `ResourceController` will use the templates namespace you have configured for the resource. You can easily change that per route, but it is also easy when you generate the routing:

Following templates will be used for actions:

* `:templates/Admin/Book:show.html.twig`
* `:templates/Admin/Book:index.html.twig`
* `:templates/Admin/Book:create.html.twig`
* `:templates/Admin/Book:update.html.twig`

### Using a Custom Form

If you want to use a custom form:

`create` and `update` actions will use `App/Form/Type/AdminBookType` form type.

#### **Note**

Remember, that if your form type has some dependencies you have to declare it as a service and tag with **name: form.type**. You can read more about it [here](http://docs.sylius.com/en/latest/components_and_bundles/bundles/SyliusResourceBundle/forms.html#custom-resource-form)

### Using a Custom Redirect

By default, after successful resource creation or update, Sylius will redirect to the `show` route and fallback to `index` if it does not exist. If you want to change that behavior, use the following configuration:

### API Versioning

One of the ResourceBundle dependencies is JMSSerializer, which provides a useful functionality of [object versioning](http://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies#versioning-objects). It is possible to take an advantage of it almost out of the box. If you would like to return only the second version of your object serializations, use the following snippet:

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book:
    resource: |
        alias: app.book
        serialization_version: 2
    type: sylius.resource_api
```

{% endcode %}

What is more, you can use a path variable to dynamically change your request. You can achieve this by setting a path prefix when importing file or specify it in the path option.

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book:
    resource: |
        alias: app.book
        serialization_version: $version
    type: sylius.resource_api
```

{% endcode %}

#### **Note**

Remember that a dynamically resolved `books` prefix is no longer available when you specify `path`, and it has to be defined manually.

### Using a Custom Criteria

Sometimes it is convenient to add some additional constraint when resolving resources. For example, one could want to present a list of all books from some library (which id would be a part of path). Assuming that the path prefix is `/libraries/{libraryId}`, if you would like to list all books from this library, you could use the following snippet:

Which will result in the following routes:

```bash
php bin/console debug:router
```

```
------------------------ --------------- -------- ------ ---------------------------------------
Name                     Method          Scheme   Host   Path
------------------------ --------------- -------- ------ ---------------------------------------
app_book_index           GET             ANY      ANY    /libraries/{libraryId}/books/
app_book_create          GET|POST        ANY      ANY    /libraries/{libraryId}/books/new
app_book_update          GET|PUT|PATCH   ANY      ANY    /libraries/{libraryId}/books/{id}/edit
app_book_show            GET             ANY      ANY    /libraries/{libraryId}/books/{id}
app_book_bulk_delete     DELETE          ANY      ANY    /libraries/{libraryId}/books/bulk-delete
app_book_delete          DELETE          ANY      ANY    /libraries/{libraryId}/books/{id}
```

### Using a Custom Identifier

As you could notice the generated routing resolves resources by the `id` field. But sometimes it is more convenient to use a custom identifier field instead, let's say a `code` (or any other field of your choice which can uniquely identify your resource). If you want to look for books by `isbn`, use the following configuration:

Which will result in the following routes:

```bash
php bin/console debug:router
```

```
------------------------ --------------- -------- ------ -------------------------
Name                     Method          Scheme   Host   Path
------------------------ --------------- -------- ------ -------------------------
app_book_index           GET             ANY      ANY    /books/
app_book_create          GET|POST        ANY      ANY    /books/new
app_book_update          GET|PUT|PATCH   ANY      ANY    /books/{isbn}/edit
app_book_show            GET             ANY      ANY    /books/{isbn}
app_book_bulk_delete     DELETE          ANY      ANY    /books/bulk-delete
app_book_delete          DELETE          ANY      ANY    /books/{isbn}
```

[**Go back to the documentation's index**](/resource/index/index)

</details>


# Forms

{% hint style="warning" %}
This section is deprecated. However, as of now, the Sylius E-Commerce project is still resorting to this configuration so you might want to check it out.
{% endhint %}

Have you noticed how Sylius generates forms for you? Of course, for many use-cases you may want to create a custom form.

## Custom Resource Form

### Create a FormType class for your resource

{% code title="src/Form/Type/BookType.php" lineNumbers="true" %}

```php
namespace App\Form\Type;

use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType;
use Symfony\Component\Form\FormBuilderInterface;

class BookType extends AbstractResourceType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        // Build your custom form, with all fields that you need
        $builder->add('title', TextType::class);
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return 'app_book';
    }
}
```

{% endcode %}

### **Note**

The getBlockPrefix method returns the prefix of the template block name for this type.

### Register the FormType as a service

### **Warning**

the registration of a form type is only needed when the form is extending the `AbstractResourceType` or when it has some custom constructor dependencies.

```yaml
app.book.form.type:
    class: App\Form\Type\BookType
    tags:
        - { name: form.type }
    arguments: ['%app.model.book.class%', '%app.book.form.type.validation_groups%']
```

## Configure the form for your resource

{% code title="config/routes/sylius\_resource.yaml" lineNumbers="true" %}

```yaml
sylius_resource:
    resources:
        app.book:
            classes:
                model: App\Entity\Book
                form: App\Form\Type\BookType
```

{% endcode %}

That's it. Your new class will be used for all forms!

[**Go back to the documentation's index**](/resource/index/index)


# Getting a Single Resource

{% hint style="warning" %}
This section is deprecated. However, as of now, the Sylius E-Commerce project is still resorting to this configuration so you might want to check it out.
{% endhint %}

Your newly created controller service supports basic CRUD operations and is configurable via routing.

The simplest action is **showAction**. It is used to display a single resource. To use it, the only thing you need to do is register a proper route.

Let's assume that you have a `app.book` resource registered. To display a single Book, define the following routing:

<details>

<summary>Yaml</summary>

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_show:
    path: /books/{id}
    methods: [GET]
    defaults:
        _controller: app.controller.book::showAction
```

{% endcode %}

</details>

<details>

<summary>PHP</summary>

{% code title="rc/Entity/Book.php" lineNumbers="true" %}

```php
use Sylius\Resource\Annotation\SyliusRoute;

#[SyliusRoute(
    name: 'app_book_show',
    path: '/books/{id}',
    methods: ['GET'],
    controller: 'app.controller.book::showAction',
)]
```

{% endcode %}

</details>

Done! Now when you go to `/books/3`, ResourceController will use the repository (`app.repository.book`) to find a Book with the given id (`3`). If the requested book resource does not exist, it will throw a `404 Not Found` exception.

When a Book is found, the default template will be rendered - `App:Book:show.html.twig` (like you configured it in the `config.yml`) with the Book result as the `book` variable. That's the most basic usage of the simple `showAction`.

## Using a Custom Template

Okay, but what if you want to display the same Book resource, but with a different representation in a view?

<details>

<summary>Yaml</summary>

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_admin_book_show:
    path: /admin/books/{id}
    methods: [GET]
    defaults:
        _controller: app.controller.book::showAction
        _sylius:
            template: Admin/Book/show.html.twig
```

{% endcode %}

</details>

<details>

<summary>PHP</summary>

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php

use Sylius\Resource\Annotation\SyliusRoute;

#[SyliusRoute(
    name: 'app_admin_book_show',
    path: '/admin/books/{id}',
    methods: ['GET'],
    controller: 'app.controller.book::showAction',
    template: 'Admin/Book/show.html.twig',
)]
```

{% endcode %}

</details>

Nothing more to do here, when you go to `/admin/books/3`, the controller will try to find the Book and render it using the custom template you specified under the route configuration. Simple, isn't it?

## Overriding Default Criteria

Displaying books by id can be boring... and let's say we do not want to allow viewing disabled books. There is a solution for that!

<details>

<summary>Yaml</summary>

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_show:
    path: /books/{title}
    methods: [GET]
    defaults:
        _controller: app.controller.book::showAction
        _sylius:
            criteria:
                title: $title
                enabled: true
```

{% endcode %}

</details>

<details>

<summary>PHP</summary>

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php

use Sylius\Resource\Annotation\SyliusRoute;

#[SyliusRoute(
    name: 'app_book_show',
    path: '/books/{title}',
    methods: ['GET'],
    controller: 'app.controller.book::showAction',
    criteria: [
        'title' => '$title',
        'enabled' => true
    ],
)]
```

{% endcode %}

</details>

With this configuration, the controller will look for a book with the given title and exclude disabled books. Internally, it simply uses the `$repository->findOneBy(array $criteria)` method to look for the resource.

## Using Custom Repository Methods

By default, resource repository uses **findOneBy(array $criteria)**, but in some cases it's not enough - for example - you want to do proper joins or use a custom query. Creating yet another action to change the method called could be a solution but there is a better way. The configuration below will use a custom repository method to get the resource.

<details>

<summary>Yaml</summary>

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_show:
    path: /books/{author}
    methods: [GET]
    defaults:
        _controller: app.controller.book::showAction
        _sylius:
            repository:
                method: findOneNewestByAuthor
                arguments: [$author]
```

{% endcode %}

</details>

<details>

<summary>PHP</summary>

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
use Sylius\Resource\Annotation\SyliusRoute;

#[SyliusRoute(
    name: 'app_book_show',
    path: '/books/{author}',
    methods: ['GET'],
    controller: 'app.controller.book::showAction',
    repository: [
        'method' => 'findOneNewestByAuthor',
        'arguments' => ['$author'],
    ],
)]
```

{% endcode %}

</details>

Internally, it simply uses the `$repository->findOneNewestByAuthor($author)` method, where `author` is taken from the current request.

## Using Custom Repository Service

If you would like to use your own service to get the resource, then try the following configuration:

<details>

<summary>Yaml</summary>

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_show:
    path: /books/{author}
    methods: [GET]
    defaults:
        _controller: app.controller.book::showAction
        _sylius:
            repository:
                method: ["expr:service('app.repository.custom_book_repository')", "findOneNewestByAuthor"]
                arguments: [$author]
```

{% endcode %}

</details>

<details>

<summary>PHP</summary>

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
use Sylius\Resource\Annotation\SyliusRoute;

#[SyliusRoute(
    name: 'app_book_show',
    path: '/books/{author}',
    methods: ['GET'],
    controller: 'app.controller.book::showAction',
    repository: [
        'method' => ["expr:service('app.repository.custom_book_repository')", "findOneNewestByAuthor"],
        'arguments' => ['$author'],
    ],
)]
```

{% endcode %}

</details>

With this configuration, method `findOneNewestByAuthor` from service with ID `app.repository.custom_book_repository` will be called to get the resource.

## Configuration Reference

<details>

<summary>Yaml</summary>

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_show:
    path: /books/{author}
    methods: [GET]
    defaults:
        _controller: app.controller.book::showAction
        _sylius:
            template: Book/show.html.twig
            repository:
                method: findOneNewestByAuthor
                arguments: [$author]
            criteria:
                enabled: true
            serialization_groups: [Custom, Details]
            serialization_version: 1.0.2
```

{% endcode %}

</details>

<details>

<summary>PHP</summary>

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
use Sylius\Resource\Annotation\SyliusRoute;

#[SyliusRoute(
    name: 'app_book_show',
    path: '/books/{author}',
    methods: ['GET'],
    controller: 'app.controller.book::showAction',
    repository: [
        'method' => 'findOneNewestByAuthor',
        'arguments' => ['$author'],
    ],
    criteria: [
        'enabled' => true,
    ],
    serializationGroups: ['Custom', 'Details'],
    serializationVersion: '1.0.2',
)]
```

{% endcode %}

</details>

Remember that you can use controller's Fully Qualified Class Name (`App\Controller\BookController`) instead of id `app.controller.book`

[**Go back to the documentation's index**](/resource/index/index)


# Getting a Collection of Resources

{% hint style="warning" %}
This section is deprecated. However, as of now, the Sylius E-Commerce project is still resorting to this configuration so you might want to check it out.
{% endhint %}

{% hint style="warning" %}
This section is deprecated. However, as of now, the Sylius E-Commerce project is still resorting to this configuration so you might want to check it out.
{% endhint %}

To get a paginated list of Books, we will use **indexAction** of our controller. In the default scenario, it will return an instance of paginator, with a list of Books.

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_index:
    path: /books
    methods: [GET]
    defaults:
        _controller: app.controller.book::indexAction
```

{% endcode %}

When you go to `/books`, the ResourceController will use the repository (`app.repository.book`) to create a paginator. The default template will be rendered - `App:Book:index.html.twig` with the paginator as the `books` variable.

A paginator can be a simple array, if you disable the pagination, otherwise it is an instance of `Pagerfanta\Pagerfanta` which is a [Library](https://github.com/BabDev/Pagerfanta) used to manage the pagination.

## Overriding the Template and Criteria

Just like for the **showAction**, you can override the default template and criteria.

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_index_inactive:
    path: /books/disabled
    methods: [GET]
    defaults:
        _controller: app.controller.book::indexAction
        _sylius:
            filterable: true
            criteria:
                enabled: false
            template: Book/disabled.html.twig
```

{% endcode %}

This action will render a custom template with a paginator only for disabled Books.

## Sorting

Except filtering, you can also sort Books.

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_index_top:
    path: /books/top
    methods: [GET]
    defaults:
        _controller: app.controller.book::indexAction
        _sylius:
            sortable: true
            sorting:
                score: desc
            template: Book/top.html.twig
```

{% endcode %}

Under that route, you can paginate over the Books by their score.

## Using a Custom Repository Method

You can define your own repository method too, you can use the same way explained in [show\_resource](http://docs.sylius.com/en/latest/components_and_bundles/bundles/SyliusResourceBundle/show_resource.html#using-custom-repository-methods).

### **Note**

If you want to paginate your resources you need to use `EntityRepository::getPaginator($queryBuilder)`. It will transform your doctrine query builder into `Pagerfanta\Pagerfanta` object.

## Changing the "Max Per Page" Option of Paginator

You can also control the "max per page" for paginator, using `paginate` parameter.

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_index_top:
    path: /books/top
    methods: [GET]
    defaults:
        _controller: app.controller.book::indexAction
        _sylius:
            paginate: 5
            sortable: true
            sorting:
                score: desc
            template: Book/top.html.twig
```

{% endcode %}

This will paginate 5 books per page, where 10 is the default.

## Disabling Pagination - Getting a Simple Collection

Pagination is handy, but you do not always want to do it, you can disable pagination and simply request a collection of resources.

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_index_top3:
    path: /books/top
    methods: [GET]
    defaults:
        _controller: app.controller.book::indexAction
        _sylius:
            paginate: false
            limit: 3
            sortable: true
            sorting:
                score: desc
            template: Book/top3.html.twig
```

{% endcode %}

That action will return the top 3 books by score, as the `books` variable.

## Changing the serialization groups of the elements in a paginated response

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_index:
    path: /{author}/books
    methods: [GET]
    defaults:
        _controller: app.controller.book::indexAction
        _sylius:
            serialization_groups: { 0: Default, items: [ Custom ] }
```

{% endcode %}

Read more about [nested serialization groups](https://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies#overriding-groups-of-deeper-branches-of-the-graph).

## Configuration Reference

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_index:
    path: /{author}/books
    methods: [GET]
    defaults:
        _controller: app.controller.book::indexAction
        _sylius:
            template: Author/books.html.twig
            repository:
                method: createPaginatorByAuthor
                arguments: [$author]
            criteria:
                enabled: true
                author.name: $author
            paginate: false # Or: 50
            limit: 100 # Or: false
            serialization_groups: [Custom, Details]
            serialization_version: 1.0.2
```

{% endcode %}

Remember that you can use controller's Fully Qualified Class Name (`App\Controller\BookController`) instead of id `app.controller.book`

[**Go back to the documentation's index**](/resource/index/index)


# Creating Resources

{% hint style="warning" %}
This section is deprecated. However, as of now, the Sylius E-Commerce project is still resorting to this configuration so you might want to check it out.
{% endhint %}

To display a form, handle its submission or to create a new resource via API, you should use the **createAction** of your **app.controller.book** service.

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml

app_book_create:
    path: /books/new
    methods: [GET, POST]
    defaults:
        _controller: app.controller.book::createAction
```

{% endcode %}

Done! Now when you go to `/books/new`, the ResourceController will use the factory (`app.factory.book`) to create a new book instance. Then it will try to create an `app_book` form, and set the newly created book as its data.

## Submitting the Form

You can use exactly the same route to handle the submit of the form and create the book.

```html
    <form method="post" action="{{ path('app_book_create') }}">
```

On submit, the create action with method POST, will bind the request on the form, and if it is valid it will use the right manager to persist the resource. Then, by default it redirects to `app_book_show` to display the created book, but you can easily change that behavior - you'll see this in further sections.

When validation fails, it will render the form just like previously with the error messages displayed.

## Changing the Template

Just like for the **show** and **index** actions, you can customize the template per route.

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_create:
    path: /books/new
    methods: [GET, POST]
    defaults:
        _controller: app.controller.book::createAction
        _sylius:
            template: Book/create.html.twig
```

{% endcode %}

## Using Custom Form

You can also use custom form type on per route basis. Following Symfony3 conventions [forms types](http://symfony.com/doc/current/forms.html#building-the-form) are resolved by FQCN. Below you can see the usage for specifying a custom form.

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_create:
    path: /books/new
    methods: [GET, POST]
    defaults:
        _controller: app.controller.book::createAction
        _sylius:
            form: App\Form\BookType
```

{% endcode %}

## Passing Custom Options to Form

What happens when you need pass some options to the form? Well, there's a configuration for that!

Below you can see the usage for specifying custom options, in this case, `validation_groups`, but you can pass any option accepted by the form.

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_create:
    path: /books/new
    methods: [GET, POST]
    defaults:
        _controller: app.controller.book::createAction
        _sylius:
            form:
                type: App\Form\BookType
                options:
                    validation_groups: [sylius, my_custom_group]
```

{% endcode %}

## Using Custom Factory Method

By default, `ResourceController` will use the `createNew` method with no arguments to create a new instance of your object. However, this behavior can be modified. To use a different method of your factory, you can simply configure the `factory` option.

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_create:
    path: /books/new
    methods: [GET, POST]
    defaults:
        _controller: app.controller.book::createAction
        _sylius:
            factory: createNewWithAuthor
```

{% endcode %}

Additionally, if you want to provide your custom method with arguments from the request, you can do so by adding more parameters.

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_create:
    path: /books/{author}/new
    methods: [GET, POST]
    defaults:
        _controller: app.controller.book::createAction
        _sylius:
            factory:
                method: createNewWithAuthor
                arguments: [$author]
```

{% endcode %}

With this configuration, `$factory->createNewWithAuthor($request->get('author'))` will be called to create new resource within the `createAction`.

## Using Custom Factory Service

If you would like to use your own service to create the resource, then try the following configuration:

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_create:
    path: /{authorId}/books/new
    methods: [GET, POST]
    defaults:
        _controller: app.controller.book::createAction
        _sylius:
            factory:
                method: ["expr:service('app.factory.custom_book_factory')", "createNewByAuthorId"]
                arguments: $authorId
```

{% endcode %}

With this configuration, service with id "app.factory.custom\_book\_factory" will be called to create new resource within the `createNewByAuthorId` method and the author id from the url as argument.

## Custom Redirect After Success

By default the controller will try to get the id of the newly created resource and redirect to the "show" route. You can easily change that behaviour. For example, to redirect to the index list after successfully creating a new resource - you can use the following configuration.

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_create:
    path: /books/new
    methods: [GET, POST]
    defaults:
        _controller: app.controller.book::createAction
        _sylius:
            redirect: app_book_index
```

{% endcode %}

You can also perform more complex redirects, with parameters. For example:

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_create:
    path: /genre/{genreId}/books/new
    methods: [GET, POST]
    defaults:
        _controller: app.controller.book::createAction
        _sylius:
            redirect:
                route: app_genre_show
                parameters: { id: $genreId }
```

{% endcode %}

In addition to the request parameters, you can access some of the newly created objects properties, using the `resource.` prefix.

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_create:
    path: /books/new
    methods: [GET, POST]
    defaults:
        _controller: app.controller.book::createAction
        _sylius:
            redirect:
                route: app_book_show
                parameters: { title: resource.title }
```

{% endcode %}

With this configuration, the `title` parameter for route `app_book_show` will be obtained from your newly created book.

## Custom Event Name

By default, there are two events dispatched during resource creation, one before adding it do database, the other after successful addition. The pattern is always the same - `{applicationName}.{resourceName}.pre/post_create`. However, you can customize the last part of the event, to provide your own action name.

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_customer_create:
    path: /customer/books/new
    methods: [GET, POST]
    defaults:
        _controller: app.controller.book::createAction
        _sylius:
            event: customer_create
```

{% endcode %}

This way, you can listen to `app.book.pre_customer_create` and `app.book.post_customer_create` events. It's especially useful, when you use `ResourceController:createAction` in more than one route.

## Configuration Reference

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_genre_book_add:
    path: /{genreName}/books/add
    methods: [GET, POST]
    defaults:
        _controller: app.controller.book::createAction
        _sylius:
            template: Book/addToGenre.html.twig
            form: App\Form\BookType
            event: book_create
            factory:
                method: createForGenre
                arguments: [$genreName]
            criteria:
                group.name: $genreName
            redirect:
                route: app_book_show
                parameters: { title: resource.title }
```

{% endcode %}

Remember that you can use controller's Fully Qualified Class Name (`App\Controller\BookController`) instead of id `app.controller.book`

[**Go back to the documentation's index**](/resource/index/index)


# Updating Resources

{% hint style="warning" %}
This section is deprecated. However, as of now, the Sylius E-Commerce project is still resorting to this configuration so you might want to check it out.
{% endhint %}

To display an edit form of a particular resource, change it or update it via API, you should use the **updateAction** action of your **app.controller.book** service.

<details>

<summary>Yaml</summary>

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_update:
    path: /books/{id}/edit
    methods: [GET, PUT]
    defaults:
        _controller: app.controller.book::updateAction
```

{% endcode %}

</details>

<details>

<summary>PHP</summary>

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
use Sylius\Resource\Annotation\SyliusRoute;

#[SyliusRoute(
    name: 'app_book_update',
    path: '/books/{id}/edit',
    methods: ['GET', 'PUT'],
    controller: 'app.controller.book::updateAction',
)]
```

{% endcode %}

</details>

Done! Now when you go to `/books/5/edit`, ResourceController will use the repository (`app.repository.book`) to find the book with id == **5**. If found it will create the `app_book` form, and set the existing book as data.

## Submitting the Form

You can use exactly the same route to handle the submit of the form and updating the book.

```html
<form method="post" action="{{ path('app_book_update', {'id': book.id}) }}">
    <input type="hidden" name="_method" value="PUT" />
```

On submit, the update action with method PUT, will bind the request on the form, and if it is valid it will use the right manager to persist the resource. Then, by default it redirects to `app_book_show` to display the updated book, but like for creation of the resource - it's customizable.

When validation fails, it will simply render the form again, but with error messages.

## Changing the Template

Just like for other actions, you can customize the template.

<details>

<summary>Yaml</summary>

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_update:
    path: /books/{id}/edit
    methods: [GET, PUT]
    defaults:
        _controller: app.controller.book::updateAction
        _sylius:
            template: Admin/Book/update.html.twig
```

{% endcode %}

</details>

<details>

<summary>PHP</summary>

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
use Sylius\Resource\Annotation\SyliusRoute;

#[SyliusRoute(
    name: 'app_book_update',
    path: '/books/{id}/edit',
    methods: ['GET', 'PUT'],
    controller: 'app.controller.book::updateAction',
    template: 'Admin/book/update.html.twig',
)]
```

{% endcode %}

</details>

## Using Custom Form

Same way like for **createAction** you can override the default form.

<details>

<summary>Yaml</summary>

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_update:
    path: /books/{id}/edit
    methods: [GET, PUT]
    defaults:
        _controller: app.controller.book::updateAction
        _sylius:
            form: App\Form\BookType
```

{% endcode %}

</details>

<details>

<summary>PHP</summary>

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
use App\Form\BookType;
use Sylius\Resource\Annotation\SyliusRoute;

#[SyliusRoute(
    name: 'app_book_update',
    path: '/books/{id}/edit',
    methods: ['GET', 'PUT'],
    controller: 'app.controller.book::updateAction',
    form: BookType::class,
)]
```

{% endcode %}

</details>

## Passing Custom Options to Form

Same way like for **createAction** you can pass options to the form.

Below you can see how to specify custom options, in this case, `validation_groups`, but you can pass any option accepted by the form.

<details>

<summary>Yaml</summary>

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_update:
    path: /books/{id}/edit
    methods: [GET, PUT]
    defaults:
        _controller: app.controller.book::updateAction
        _sylius:
            form:
                type: app_book_custom
                options:
                    validation_groups: [sylius, my_custom_group]
```

{% endcode %}

</details>

<details>

<summary>PHP</summary>

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
use App\Form\BookType;
use Sylius\Resource\Annotation\SyliusRoute;

#[SyliusRoute(
    name: 'app_book_update',
    path: '/books/{id}/edit',
    methods: ['GET', 'PUT'],
    controller: 'app.controller.book::updateAction',
    form: [
        'type' => BookType::class,
        'validation_groups' => ['sylius', 'my_custom_group'],
    ],
)]
```

{% endcode %}

</details>

## Overriding the Criteria

By default, the **updateAction** will look for the resource by id. You can easily change that criteria.

<details>

<summary>Yaml</summary>

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_update:
    path: /books/{title}/edit
    methods: [GET, PUT]
    defaults:
        _controller: app.controller.book::updateAction
        _sylius:
            criteria: { title: $title }
```

{% endcode %}

</details>

<details>

<summary>PHP</summary>

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
use App\Form\BookType;
use Sylius\Resource\Annotation\SyliusRoute;

#[SyliusRoute(
    name: 'app_book_update',
    path: '/books/{id}/edit',
    methods: ['GET', 'PUT'],
    controller: 'app.controller.book::updateAction',
    criteria: [
        'title' => '$title',
    ],
)]
```

{% endcode %}

</details>

## Custom Redirect After Success

By default the controller will try to get the id of resource and redirect to the "show" route. To change that, use the following configuration.

<details>

<summary>Yaml</summary>

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_update:
    path: /books/{id}/edit
    methods: [GET, PUT]
    defaults:
        _controller: app.controller.book::updateAction
        _sylius:
            redirect: app_book_index
```

{% endcode %}

</details>

<details>

<summary>PHP</summary>

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
use App\Form\BookType;
use Sylius\Resource\Annotation\SyliusRoute;

#[SyliusRoute(
    name: 'app_book_update',
    path: '/books/{id}/edit',
    methods: ['GET', 'PUT'],
    controller: 'app.controller.book::updateAction',
    redirect: 'app_book_index',
)]
```

{% endcode %}

</details>

You can also perform more complex redirects, with parameters. For example:

<details>

<summary>Yaml</summary>

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_update:
    path: /genre/{genreId}/books/{id}/edit
    methods: [GET, PUT]
    defaults:
        _controller: app.controller.book::updateAction
        _sylius:
            redirect:
                route: app_genre_show
                parameters: { id: $genreId }
```

{% endcode %}

</details>

<details>

<summary>PHP</summary>

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
use App\Form\BookType;
use Sylius\Resource\Annotation\SyliusRoute;

#[SyliusRoute(
    name: 'app_book_update',
    path: '/genre/{genreId}/books/{id}/edit',
    methods: ['GET', 'PUT'],
    controller: 'app.controller.book::updateAction',
    redirect: [
        'route' => 'app_genre_show',
        'parameters' => ['id' => '$genreId'],
    ],
)]
```

{% endcode %}

</details>

## Custom Event Name

By default, there are two events dispatched during resource update, one before setting new data, the other after successful update. The pattern is always the same - `{applicationName}.{resourceName}.pre/post_update`. However, you can customize the last part of the event, to provide your own action name.

<details>

<summary>Yaml</summary>

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_customer_update:
    path: /customer/book-update/{id}
    methods: [GET, PUT]
    defaults:
        _controller: app.controller.book::updateAction
        _sylius:
            event: customer_update
```

{% endcode %}

</details>

<details>

<summary>PHP</summary>

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
use App\Form\BookType;
use Sylius\Resource\Annotation\SyliusRoute;

#[SyliusRoute(
    name: 'app_book_customer_update',
    path: '/customer/book-update/{id}',
    methods: ['GET', 'PUT'],
    controller: 'app.controller.book::updateAction',
    event: 'customer_update',
)]
```

{% endcode %}

</details>

This way, you can listen to `app.book.pre_customer_update` and `app.book.post_customer_update` events. It's especially useful, when you use `ResourceController:updateAction` in more than one route.

## \[API] Returning resource or no content

Depending on your app approach it can be useful to return a changed object or only the `204 HTTP Code`, which indicates that everything worked smoothly. Sylius, by default is returning the `204 HTTP Code`, which indicates an empty response. If you would like to receive a whole object as a response you should set a `return_content` option to true.

<details>

<summary>Yaml</summary>

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_update:
    path: /books/{title}/edit
    methods: [GET, PUT]
    defaults:
        _controller: app.controller.book::updateAction
        _sylius:
            criteria: { title: $title }
            return_content: true
```

{% endcode %}

</details>

<details>

<summary>PHP</summary>

{% code title="src/Entity/Book.php" lineNumbers="true" %}

```php
use App\Form\BookType;
use Sylius\Resource\Annotation\SyliusRoute;

#[SyliusRoute(
    name: 'app_book_update',
    path: '/books/{title}/edit',
    methods: ['GET', 'PUT'],
    controller: 'app.controller.book::updateAction',
    criteria: ['title' => '$title'],
    returnContent: true,
)]
```

{% endcode %}

</details>

### **Warning**

The `return_content` flag is available for the `applyStateMachineTransitionAction` method as well. But these are the only ones which can be configured this way. It is worth noticing, that the `applyStateMachineTransitionAction` returns a default `200 HTTP Code` response with a fully serialized object.

## Configuration Reference

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_update:
    path: /genre/{genreId}/books/{title}/edit
    methods: [GET, PUT, PATCH]
    defaults:
        _controller: app.controller.book::updateAction
        _sylius:
            template: Book/editInGenre.html.twig
            form: app_book_custom
            event: book_update
            repository:
                method: findBookByTitle
                arguments: [$title, expr:service('app.context.book')]
            criteria:
                enabled: true
                genreId: $genreId
            redirect:
                route: app_book_show
                parameters: { title: resource.title }
            return_content: true
```

{% endcode %}

Remember that you can use controller's Fully Qualified Class Name (`App\Controller\BookController`) instead of id `app.controller.book`

[**Go back to the documentation's index**](/resource/index/index)


# Deleting Resources

{% hint style="warning" %}
This section is deprecated. However, as of now, the Sylius E-Commerce project is still resorting to this configuration so you might want to check it out.
{% endhint %}

Deleting a resource is simple.

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_delete:
    path: /books/{id}
    methods: [DELETE]
    defaults:
        _controller: app.controller.book::deleteAction
```

{% endcode %}

## Calling an Action with DELETE method

Currently browsers do not support the "DELETE" http method. Fortunately, Symfony has a very useful feature. You can make a POST call with parameter override, which will force the framework to treat the request as the specified method.

```html
<form method="post" action="{{ path('app_book_delete', {'id': book.id}) }}">
    <input type="hidden" name="_method" value="DELETE" />
    <button type="submit">
        Delete
    </button>
</form>
```

On submit, the delete action with the method DELETE, will remove and flush the resource. Then, by default it redirects to `app_book_index` to display the books index, but just like for the other actions - it's customizable.

## Overriding the Criteria

By default, the **deleteAction** will look for the resource by id. However, you can easily change that. For example, if you want to delete a book that belongs to a particular genre, not only by its id.

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_delete:
    path: /genre/{genreId}/books/{id}
    methods: [DELETE]
    defaults:
        _controller: app.controller.book::deleteAction
        _sylius:
            criteria:
                id: $id
                genre: $genreId
```

{% endcode %}

There are no magic hacks behind that, it simply takes parameters from request and builds the criteria array for the `findOneBy` repository method.

## Custom Redirect After Success

By default the controller will redirect to the "index" route after successful action. To change that, use the following configuration.

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_delete:
    path: /genre/{genreId}/books/{id}
    methods: [DELETE]
    defaults:
        _controller: app.controller.book::deleteAction
        _sylius:
            redirect:
                route: app_genre_show
                parameters: { id: $genreId }
```

{% endcode %}

## Custom Event Name

By default, there are two events dispatched during resource deletion, one before removing, the other after successful removal. The pattern is always the same - `{applicationName}.{resourceName}.pre/post_delete`. However, you can customize the last part of the event, to provide your own action name.

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_book_customer_delete:
    path: /customer/book-delete/{id}
    methods: [DELETE]
    defaults:
        _controller: app.controller.book::deleteAction
        _sylius:
            event: customer_delete
```

{% endcode %}

This way, you can listen to `app.book.pre_customer_delete` and `app.book.post_customer_delete` events. It's especially useful, when you use `ResourceController:deleteAction` in more than one route.

## Configuration Reference

{% code title="config/routes.yaml" lineNumbers="true" %}

```yaml
app_genre_book_remove:
    path: /{genreName}/books/{id}/remove
    methods: [DELETE]
    defaults:
        _controller: app.controller.book::deleteAction
        _sylius:
            event: book_delete
            repository:
                method: findByGenreNameAndId
                arguments: [$genreName, $id]
            criteria:
                genre.name: $genreName
                id: $id
            redirect:
                route: app_genre_show
                parameters: { genreName: $genreName }
```

{% endcode %}

Remember that you can use controller's Fully Qualified Class Name (`App\Controller\BookController`) instead of id `app.controller.book`

[**Go back to the documentation's index**](/resource/index/index)


# Configuring a state machine

{% hint style="warning" %}
This section is deprecated. However, as of now, the Sylius E-Commerce project is still resorting to this configuration so you might want to check it out.
{% endhint %}

You can either use [Symfony workflow](https://symfony.com/doc/current/components/workflow.html) or [Winzou state machine](https://github.com/winzou/StateMachineBundle). The recommended way is to use the `Symfony workflow component`.

If only Symfony workflow is on your requirements you have nothing to do.

But you can configure it explicitly:

## Configuring Symfony workflow as state machine\`

```yaml
sylius_resource:
    settings:
        state_machine_component: symfony
```

## Configuring Winzou as state machine\`

If Winzou state machine is on your requirements you have nothing to do even if Symfony workflow is on your requirements too.

But you can configure it explicitly:

```yaml
sylius_resource:
    settings:
        state_machine_component: winzou
```

## Applying a transition\`

You can create a route to apply any transition

```yaml
app_pull_request_apply_transition:
    path: /pull-requests/{id}/{transition}
    methods: [PUT]
    defaults:
        _controller: app.controller.pull_request:applyStateMachineTransitionAction
        _sylius:
            state_machine:
                #graph: pull_request # name of the graph for Winzou or workflow name for Symfony (optional)
                transition: $transition
```


# Configuration Reference

### Configuration Reference

{% hint style="warning" %}
This section is deprecated. However, as of now, the Sylius E-Commerce project is still resorting to this configuration so you might want to check it out.
{% endhint %}

```yaml
sylius_resource:
    resources:
        app.book:
            driver: doctrine/orm
            classes:
                model: # Required!
                interface: ~
                controller: Sylius\Bundle\ResourceBundle\Controller\ResourceController
                repository: ~
                factory: Sylius\Component\Resource\Factory\Factory
                form: Sylius\Bundle\ResourceBundle\Form\Type\DefaultResourceType
                    validation_groups: [sylius]
            templates:
                form: Book/_form.html.twig
            translation:
                classes:
                    model: ~
                    interface: ~
                    controller: Sylius\Bundle\ResourceBundle\Controller\ResourceController
                    repository: ~
                    factory: Sylius\Component\Resource\Factory\Factory
                    form: Sylius\Bundle\ResourceBundle\Form\Type\DefaultResourceType
                        validation_groups: [sylius]
                templates:
                    form: Book/Translation/_form.html.twig
```

### Routing Generator Configuration Reference

```yaml
app_book:
    resource: |
        alias: app.book
        path: library
        identifier: code
        criteria:
            code: $code
        section: admin
        templates: :Book
        form: App/Form/Type/SimpleBookType
        redirect: create
        except: ['show']
        only: ['create', 'index']
        serialization_version: 1
    type: sylius.resource
```

[**Go back to the documentation's index**](/resource/index/index)


# Grid Bundle documentation

Displaying a grid with sorting and filtering options is a common task for many web applications. This bundle integrates the Sylius Grid component with the Symfony framework and allows you to display grids really easily.

Some of the features worth mentioning:

* Uses YAML or PHP to define the grid structure
* Supports different data sources: Doctrine ORM/ODM, native SQL query.
* Rich filter functionality, easy to define your own filter type with flexible form
* Each column type is configurable and you can create your own
* Automatic sorting

## Menu

* [Installation](/grid/index/installation)
* [Creating your first grid](/grid/index/your_first_grid)
* [Fields](/grid/index/fields)
* [Filters](/grid/index/filters)
* [Actions](/grid/index/actions)
* [Mutators](/grid/index/mutators)
* [Advanced configuration](/grid/index/advanced_configuration)
* [Configuration Reference](/grid/index/configuration)


# Installation

We assume you're familiar with [Composer](http://packagist.org), a dependency manager for PHP. Use the following command to add the bundle to your composer.json and download the package.

If you have [Composer installed globally](http://getcomposer.org/doc/00-intro.md#globally).

```bash
composer require sylius/grid-bundle
```

Otherwise, you have to download a .phar file.

```bash
curl -sS https://getcomposer.org/installer | php
php composer.phar require sylius/grid-bundle
```

## Adding required bundles to the kernel

You need to enable the bundle inside the kernel.

If you're not using any other Sylius bundles, you will also need to add `SyliusResourceBundle` and its dependencies to kernel. Don't worry, everything was automatically installed via Composer.

{% code title="config/bundles.php" %}

```php
<?php

return [
    Sylius\Bundle\GridBundle\SyliusGridBundle::class => ['all' => true],
];
```

{% endcode %}

Congratulations! The bundle is now installed and ready to use. You need to define your first resource and grid!


# Creating your first grid

In order to use grids, you need to register your entity as a Sylius resource. Let us assume you have a Supplier model in your application, which represents a supplier of goods in your shop and has several fields, including *name*, *description* and *enabled*.

In order to make it a Sylius resource, you need to add the `AsResource` attribute and implement `ResourceInterface`.

{% code title="src/Entity/Supplier.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource]
class Supplier implements ResourceInterface
{
    // ...
}
```

{% endcode %}

That's it! Your class is now a resource. In order to learn what it means, please refer to the [SyliusResourceBundle](/resource/index) documentation.

## Grid Maker

You can create your grid using the [Symfony Maker bundle](https://symfony.com/bundles/SymfonyMakerBundle/current/index.html).

{% hint style="info" %}
Since SyliusGridBundle 1.14, this command supports any PHP class, including Sylius Resources too !
{% endhint %}

```shell
$ bin/console make:grid
```

{% hint style="info" %}
This command will generate a grid with a field entry for each of your PHP class properties except `id`.
{% endhint %}

## Grid Definition

Now we can configure our first grid:

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="/src/Grid/AdminSupplierGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\Supplier;
use Sylius\Bundle\GridBundle\Builder\Field\StringField;
use Sylius\Bundle\GridBundle\Builder\Field\TwigField;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    name: 'app_admin_supplier',
    resourceClass: Supplier::class
)]
final class AdminSupplierGrid extends AbstractGrid
{

    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
         $gridBuilder
            ->withFields(
                StringField::create('name')
                    ->setLabel('app.ui.name'),
                TwigField::create('enabled', '@SyliusBootstrapAdminUi/shared/grid/field/boolean.html.twig')
                    ->setLabel('app.ui.enabled'),
            )
         ;
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code lineNumbers="true" %}

```php
<?php 

use App\Entity\Supplier;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Builder\Field\StringField;
use Sylius\Bundle\GridBundle\Builder\Field\TwigField;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid) {
    $grid->addGrid(GridBuilder::create('app_admin_supplier', Supplier::class)
        ->withFields(
            StringField::create('name')
                ->setLabel('app.ui.name'),
            TwigField::create('enabled', '@SyliusBootstrapAdminUi/shared/grid/field/boolean.html.twig')
                ->setLabel('app.ui.enabled'),    
        )
    )
};
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_admin_supplier:
            driver:
                name: doctrine/orm
                options:
                    class: App\Entity\Supplier
            fields:
                name:
                    type: string
                    label: app.ui.name
                enabled:
                    type: twig
                    label: app.ui.enabled
                    options:
                        template: '@SyliusBootstrapAdminUi/shared/grid/field/boolean.html.twig' # This will be a checkbox field
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% hint style="info" %}
Available field types are `string`, `datetime`, `callable` and `twig`. The Twig field type is particularly powerful and can be leveraged to render more complex fields (booleans, combinations of resource properties etc) and gives you full flexibility of styling.
{% endhint %}

## Using your grid on an index operation

The `SyliusResourceBundle` allows you to use a grid into an index operation:

{% code title="src/Entity/Supplier.php" lineNumbers="true" %}

```php
namespace App\Entity;

use App\Grid\AdminSupplierGrid;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Index;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    section: 'admin', // This will influence the route name
    routePrefix: '/admin',
    templatesDir: '@SyliusAdminUi/crud', // This directory contains the generic template for your list
    operations: [
        // You can use either the FQCN of your grid
        new Index(grid: AdminSupplierGrid::class)
        // Or you can use the grid name
        new Index(grid: 'app_admin_supplier')
    ],
)]
class Supplier implements ResourceInterface
{
    // ...
}
```

{% endcode %}

{% hint style="info" %}
Note: When you are in a Sylius project, the `templatesDir` path is: `@SyliusAdmin/shared/crud`
{% endhint %}

This will generate the following path:

```shell
 ------------------------------ ---------------------------
  Name                           Path                                           
 ------------------------------ ---------------------------                  
  app_admin_supplier_index           /admin/suppliers               
```

{% hint style="info" %}
See how to add this new page into your [administration menu](/cookbook/admin_panel/menu).
{% endhint %}

Now, your new grid should look like this when accessing the index on */admin/suppliers/*:

![image](/files/CU7voY3zwMZ9n1an82LR)

## Defining Filters

To allow users to search for specific items in the grid, you can use filters.

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/AdminSupplierGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\Supplier;
use Sylius\Bundle\GridBundle\Builder\Filter\BooleanFilter;
use Sylius\Bundle\GridBundle\Builder\Filter\StringFilter;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;

#[AsGrid(
    name: 'app_admin_supplier',
    resourceClass: Supplier::class,
)]
final class AdminSupplierGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFilters(
                StringFilter::create('name')
                    ->setLabel('Name'),
                BooleanFilter::create('enabled')   
                    ->setLabel('Enabled'),
            )
        ;
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code lineNumbers="true" %}

```php
<?php

use App\Entity\Supplier;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Builder\Filter\BooleanFilter;
use Sylius\Bundle\GridBundle\Builder\Filter\StringFilter;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid) {
    $grid->addGrid(GridBuilder::create('app_admin_supplier', Supplier::class)
        ->withFilters(
            StringFilter::create('name')
                ->setLabel('Name'),
            BooleanFilter::create('enabled')
               ->setLabel('Enabled'),    
        )
    )
};
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_admin_supplier:
            # ...
            filters:
                name:
                    type: string
                    label: Name
                enabled:
                    type: boolean
                    label: Enabled
```

{% endcode %}
{% endtab %}
{% endtabs %}

What will it look like in the admin panel?

![image](/files/UqKN4TGkEGweqfnXRwt2)

### Advanced filtering : relationships

What about filtering by fields of related entities? For instance if you would like to filter your suppliers by their country of origin, which is a property of the associated address entity.

There are 2 ways you can do this :

* you can resort to a custom [repository method](https://docs.sylius.com/en/latest/customization/repository.html) and pass it to the Grid Builder via `setRepositoryMethod()` but this will only work when using Doctrine
* or you can join on your entities directly inside your provider when using a custom data provider

#### Custom repository method (Doctrine-only)

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/AdminSupplierGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\Supplier;
use Sylius\Bundle\GridBundle\Builder\Filter\BooleanFilter;
use Sylius\Bundle\GridBundle\Builder\Filter\StringFilter;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;

#[AsGrid(
    name: 'app_admin_supplier',
    resourceClass: Supplier::class,
)]
final class AdminSupplierGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder->setRepositoryMethod('mySupplierGridQuery');
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use App\Entity\Supplier;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Builder\Filter\BooleanFilter;
use Sylius\Bundle\GridBundle\Builder\Filter\StringFilter;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid) {
    $grid->addGrid(GridBuilder::create('app_admin_supplier', Supplier::class)
        ->setRepositoryMethod('mySupplierGridQuery')
    )
};
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_admin_supplier:
            driver:
                name: doctrine/orm
                options:
                    class: App\Entity\Supplier
                    repository:
                        method: mySupplierGridQuery
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% hint style="info" %}
The repository method must return a `QueryBuilder` object, as the query needs to adjust based on the filters and sorting the user will apply later. Furthermore, all sub-entities you wish to use later on for filtering must be joined explicitly in the query.
{% endhint %}

#### Custom data provider

Here is a simple example of a custom data provider. You're obviously free to actually fetch your data in whatever way suits your need, by calling a repository of any kind (Doctrine, API, in-memory,..) directly or call a query bus for instance.

{% code title="src/Grid/Provider/SupplierGridProvider.php" lineNumbers="true" %}

```php
<?php

namespace App\Grid\Provider;

use App\Repository\SupplierRepository;
use App\Resource\SupplierResource;
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 SupplierGridProvider implements DataProviderInterface
{
    public function __construct(
        private SupplierRepository $supplierRepository,
    ) {
    }

    public function getData(Grid $grid, Parameters $parameters): PagerFantaInterface
    {
        $page = (int) $parameters->get('page', 1);
        $itemsPerPage = (int) $parameters->get('limit', 10);
        $criteria = $parameters->get('criteria');

        $paginator = $this->getSuppliersPaginator($page, $itemsPerPage, $criteria);

        $data = [];

        foreach ($paginator as $row) {
            $data[] = new SupplierResource(
                enabled: $row['enabled'],
                name: $row['name'],
               // ...
            );
        }

        return new Pagerfanta(new FixedAdapter($paginator->count(), $data));
    }

    public function getSuppliersPaginator(int $page, int $itemsPerPage, ?array $criteria): PagerfantaInterface
    {
        $supplierRepository = $this->supplierRepository;

        if (!empty($criteria['country'] ?? null)) {
            $supplierRepository = $supplierRepository->withCountryCode($criteria['country']);
        }

        return $supplierRepository->withPagination($page, $itemsPerPage)->paginator();
    }
}
```

{% endcode %}

Then, this example Doctrine repository uses a JOIN statement on our related Address Entity.

{% code title="src/Repository/SupplierRepository.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Repository;

use App\Doctrine\DoctrineRepository;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;

// this example DoctrineRepository would manage boilerplate iterators, pagination etc 
final class SupplierRepository extends DoctrineRepository 
{

    private const string ENTITY_CLASS = Supplier::class;
    private const string ALIAS = 'supplier';
    private const string ADDRESS_ALIAS = 'address';

    public function __construct(EntityManagerInterface $em)
    {
        parent::__construct($em, self::ENTITY_CLASS, self::ALIAS);

        $this->queryBuilder
            ->innerJoin(sprintf('%s.address', self::ALIAS), self::ADDRESS_ALIAS);
    }
    
    public function withCountryCode(string $countryCode): static
    {
        return $this->filter(static function (QueryBuilder $queryBuilder) use ($countryCode): void {
            $queryBuilder
                ->andWhere($queryBuilder->expr()->eq(sprintf('%s.countryCode', self::ADDRESS_ALIAS), ':countryCode'))
                ->setParameter('countryCode', $countryCode)
            ;
        });
    }
    
    // ....
    
}
```

{% endcode %}

#### Adding your entity filter to your grid

Then you can simply insert your filter inside the grid.

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/AdminSupplierGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\Supplier;
use App\Grid\Provider\SupplierGridProvider;
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(
    name: 'app_admin_supplier',
    provider: SupplierGridProvider::class, // only needed if you used a custom provider
    resourceClass: Supplier::class, // only needed if you did NOT use a provider
)]
final class AdminSupplierGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFilters(
                StringFilter::create('country', ['address.country'], 'contains')
                    ->setLabel('origin')
            )
        ;
    }
    
    // ...
}
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use App\Entity\Supplier;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Builder\Filter\StringFilter;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid) {
    $grid->addGrid(GridBuilder::create('app_admin_supplier', Supplier::class)
        ->withFilters(
            StringFilter::create('country', ['address.country'], 'contains')
                ->setLabel('origin')
        )
    )
};
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_admin_supplier:
            # ...
            filters:
                # ...
                country:
                    type: string
                    label: origin
                    options:
                        fields: [address.country]
                    form_options:
                        type: contains
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Default Sorting

You can define by which field you want the grid to be sorted and how using `orderBy()` .

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/AdminSupplierGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\Supplier;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Bundle\GridBundle\Grid\ResourceAwareGridInterface;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    name: 'app_admin_supplier',
    resourceClass: Supplier::class,
)]
final class AdminSupplierGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder->orderBy(name: 'name', direction: 'asc');
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use App\Entity\Supplier;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid) {
    $grid->addGrid(GridBuilder::create('app_admin_supplier', Supplier::class)
        ->orderBy('name', 'asc')
    )
};
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_admin_supplier:
            # ...
            sorting:
                name: asc
                # ...
```

{% endcode %}
{% endtab %}
{% endtabs %}

Then in the fields section, indicate that the field can be used for sorting with `setSortable()`:

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/AdminSupplierGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\Supplier;
use Sylius\Bundle\GridBundle\Builder\Field\StringField;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    name: 'app_admin_supplier',
    resourceClass: Supplier::class,
)]
final class AdminSupplierGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFields(
                StringField::create('name')
                    ->setLabel('sylius.ui.name')
                    ->setSortable(true)
            )
        ;
    }

}
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use App\Entity\Supplier;
use Sylius\Bundle\GridBundle\Builder\Field\StringField;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid) {
    $grid->addGrid(GridBuilder::create('app_admin_supplier', Supplier::class)
        ->withFields(
            StringField::create('name')
                ->setLabel('sylius.ui.name')
                ->setSortable(true)
        )
    )
};
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_admin_supplier:
            # ...
            fields:
                name:
                    type: string
                    label: sylius.ui.name
                    sortable: ~
                # ...
```

{% endcode %}
{% endtab %}
{% endtabs %}

If your field is not of a "simple" type, e.g. a Twig template with a specific path, you can enable sorting with the following definition:

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/AdminSupplierGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\Supplier;
use Sylius\Bundle\GridBundle\Builder\Field\TwigField;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    name: 'app_admin_supplier',
    resourceClass: Supplier::class,
)]
final class AdminSupplierGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFields(
                TwigField::create('name', '@App/Grid/Fields/my_country_flags.html.twig')
                    ->setPath('address.country')
                    ->setLabel('app.ui.country')
                    ->setSortable(true, 'address.country')
            )
        ;
    } 
}
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use App\Entity\Supplier;
use Sylius\Bundle\GridBundle\Builder\Field\TwigField;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid) {
    $grid->addGrid(GridBuilder::create('app_admin_supplier', Supplier::class)
        ->withFields(
            TwigField::create('name', '@App/Grid/Fields/myCountryFlags.html.twig')
                ->setPath('address.country')
                ->setLabel('app.ui.country')
                ->setSortable(true, 'address.country')
        )
    )
};
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_admin_supplier:
            # ...
            fields:
                # ...
                origin:
                    type: twig
                    options:
                        template: "@App/Grid/Fields/myCountryFlags.html.twig"
                    path: address.country
                    label: app.ui.country
                    sortable: address.country
                # ...
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Pagination

You can limit how many items are visible on each page by providing an array of integers into the `limits` parameter. The first element of the array will be treated as the default.

{% hint style="info" %}
Pagination limits are set by default to 10, 25 and 50 items per page. In order to turn it off, configure `limits: ~` .
{% endhint %}

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/AdminSupplierGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\Supplier;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    name: 'app_admin_supplier',
    resourceClass: Supplier::class,
)]
final class AdminSupplierGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder->setLimits([12, 24, 48]);
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" %}

```yaml
sylius_grid:
    grids:
        app_admin_supplier:
            limits: 
                - 12
                - 24
                - 48
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code title="config/packages/sylius\_grid.php" %}

```php
<?php

use App\Entity\Supplier;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid) {
    $grid->addGrid(
        GridBuilder::create('app_admin_supplier', Supplier::class)
                        ->setLimits([12, 24, 48])
    );
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Actions Configuration

Next step is adding some actions to the grid: create, update and delete.

First, we need to create these operations on our resource:

{% code title="src/Entity/Supplier.php" lineNumbers="true" %}

```php
namespace App\Entity;

use App\Grid\AdminSupplierGrid;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Metadata\Delete;
use Sylius\Resource\Metadata\Index;
use Sylius\Resource\Metadata\Update;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    // ...
    operations: [
        new Create(),
        new Update(),
        new Delete(),
        // ...
    ],
)]
class Supplier implements ResourceInterface
{
    // ...
}
```

{% endcode %}

These new operations are now available:

```shell
 ------------------------------ -----------------------------
  Name                           Path                                           
 ------------------------------ -----------------------------         
  app_admin_supplier_create     /admin/suppliers/new                           
  app_admin_supplier_update     /admin/suppliers/{id}/edit                     
  app_admin_supplier_delete     /admin/suppliers/{id}/delete                   
  app_admin_supplier_index      /admin/suppliers                 
```

Then we need to add these operations into our Grid using Actions.

{% hint style="info" %}
There are two types of actions that can be added to a grid:

* `main` actions affect the entire grid, such as adding new items or deleting items in bulk
* and `item` actions which apply to a single row of the grid (one object), such as editing or deleting.
  {% endhint %}

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/AdminSupplierGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\Supplier;
use Sylius\Bundle\GridBundle\Builder\Action\CreateAction;
use Sylius\Bundle\GridBundle\Builder\Action\DeleteAction;
use Sylius\Bundle\GridBundle\Builder\Action\UpdateAction;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    name: 'app_admin_supplier',
    resourceClass: Supplier::class,
)]
final class AdminSupplierGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withMainActions(
                CreateAction::create(),
            )
            ->withItemActions(
                UpdateAction::create(),
                DeleteAction::create(),
            )
        ;
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use App\Entity\Supplier;
use Sylius\Bundle\GridBundle\Builder\Action\CreateAction;
use Sylius\Bundle\GridBundle\Builder\Action\DeleteAction;
use Sylius\Bundle\GridBundle\Builder\Action\UpdateAction;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid) {
    $grid->addGrid(GridBuilder::create('app_admin_supplier', Supplier::class)
        ->withMainActions(
            CreateAction::create(),
        )
        ->withItemActions(
            UpdateAction::create(),
            DeleteAction::create(),
        )
    )
};
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_admin_supplier:
            # ...
            actions:
                main:
                    create:
                        type: create
                item:
                    update:
                        type: update
                    delete:
                        type: delete
```

{% endcode %}
{% endtab %}
{% endtabs %}

This activates such a view on the `/admin/suppliers/` path:

![image](/files/0exhkzZBGChe16eUwuQQ)

Your grid is ready to use!


# Fields

## Configuring Fields

Each field can be configured with several configuration keys, to make it more suitable to your grid requirements.

| Name     | Type   | Description                                                                                                                   |
| -------- | ------ | ----------------------------------------------------------------------------------------------------------------------------- |
| type     | string | Type of column. Default field types are described [here](https://github.com/Sylius/Stack/tree/main/docs/grid/field_types.md). |
| label    | string | Label displayed in the field header. By default, it is field name.                                                            |
| path     | string | Path to property displayed in field (can be property of resource or one of its referenced objects).                           |
| position | int    | Position of field in the grid index view.                                                                                     |
| options  | array  | Array of field options (see below).                                                                                           |

The `options` field can itself contain the following fields:

| Name     | Type   | Description                                                                                                 | Default         |
| -------- | ------ | ----------------------------------------------------------------------------------------------------------- | --------------- |
| template | string | Available (and required) only for *twig* column type. Path to template that is used to render column value. |                 |
| format   | string | Available only for *datetime* field type.                                                                   | `Y:m:d` `H:i:s` |

## Field types

This is the list of built-in field types.

### String

The simplest column type, which displays the value at the specified path as plain text.

By default, it uses the name of the field, but you can specify a different path if needed. For example:

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/UserGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\User;
use Sylius\Bundle\GridBundle\Builder\Field\StringField;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    name: 'app_user',
    resourceClass: User::class
)]
final class UserGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFields(
                StringField::create('email')
                    ->setLabel('app.ui.email') // # each field type can have a label, we suggest using translation keys instead of messages
                    ->setPath('contactDetails.email')
            )
        ;
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_user:
            fields:
                email:
                    type: string
                    label: app.ui.email # each field type can have a label, we suggest using translation keys instead of messages
                    path: contactDetails.email
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use Sylius\Bundle\GridBundle\Builder\Field\StringField;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid): void {
    $grid->addGrid(GridBuilder::create('app_user', '%app.model.user.class%')
        ->withFields(
            StringField::create('email')
                ->setLabel('app.ui.email') // # each field type can have a label, we suggest using translation keys instead of messages
                ->setPath('contactDetails.email')
        )
    )
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

This configuration will display the value of `$user->getContactDetails()->getEmail()`.

### DateTime

This column type works exactly the same way as *StringField*, but expects a *DateTime* instance and outputs a formatted date and time string.

Available options:

* `format` - defaults to `Y:m:d H:i:s`, you can set it to any supported format (see <https://www.php.net/manual/en/datetime.format.php>)
* `timezone` - defaults to `%sylius_grid.timezone%` parameter, null if such a parameter does not exist, you can set it to any supported timezone (see <https://www.php.net/manual/en/timezones.php>)

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/UserGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\User;
use Sylius\Bundle\GridBundle\Builder\Field\DateTimeField;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    name: 'app_user',
    resourceClass: User::class
)]
final class UserGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFields(
                DateTimeField::create('birthday', 'Y:m:d H:i:s', null) // this format and timezone are the default value, but you can modify them
                    ->setLabel('app.ui.birthday')
            )
        ;
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_user:
            fields:
                birthday:
                    type: datetime
                    label: app.ui.birthday
                    options:
                        format: 'Y:m:d H:i:s'
                        timezone: null
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use Sylius\Bundle\GridBundle\Builder\Field\DateTimeField;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid): void {
    $grid->addGrid(GridBuilder::create('app_user', '%app.model.user.class%')
        ->withFields(
            DateTimeField::create('birthday', 'Y:m:d H:i:s', null) // this format and timezone are the default value, but you can modify them
                ->setLabel('app.ui.birthday')
        )
    )
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% hint style="warning" %}
If you want to call the `setOptions` function, you must pass both `'format'` and `'timezone'` as arguments again. Otherwise, they will be unset.

```php
$field->setOptions([
    'format' => 'Y-m-d H:i:s',
    'timezone' => 'null'

    // Your options here
]);
```

{% endhint %}

### Twig

The Twig column type is the most flexible one, because it delegates the logic of rendering the value to the Twig templating engine. First, you must specify the template you want to render.

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/UserGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\User;
use Sylius\Bundle\GridBundle\Builder\Field\TwigField;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    name: 'app_user',
    resourceClass: User::class
)]
final class UserGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFields(
                TwigField::create('name', ':Grid/Column:_prettyName.html.twig')
                    ->setLabel('app.ui.name')
            )
        ;
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_user:
            fields:
                name:
                    type: twig
                    label: app.ui.name
                    options:
                        template: "@Grid/Column/_prettyName.html.twig"
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use Sylius\Bundle\GridBundle\Builder\Field\TwigField;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid): void {
    $grid->addGrid(GridBuilder::create('app_user', '%app.model.user.class%')
        ->withFields(
            TwigField::create('name', '@Grid/Column/_prettyName.html.twig')
                ->setLabel('app.ui.name')
        )
    )
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

Then, within the template, you can render the field's value via the `data` variable.

{% code title="@Grid/Column/\_prettyName.html.twig" %}

```twig
<strong>{{ data }}</strong>
```

{% endcode %}

#### Binding a Field to the Full Object Instance

To render more complex data in a grid field, you can bind the field to the root object by redefining the field path. This gives you access to all attributes of the underlying object when rendering the field.

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/UserGrid.php" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\User;
use Sylius\Bundle\GridBundle\Builder\Field\TwigField;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    name: 'app_user',
    resourceClass: User::class
)]
final class UserGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFields(
                TwigField::create('name', ':Grid/Column:_prettyName.html.twig')
                    ->setLabel('app.ui.name')
                    ->setPath('.') // sets the field path to the root object
            )
        ;
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" %}

```yaml
sylius_grid:
    grids:
        app_user:
            fields:
                name:
                    type: twig
                    label: app.ui.name
                    path: .    # sets the field path to the root object
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code title="config/packages/sylius\_grid.php" %}

```php
<?php

use Sylius\Bundle\GridBundle\Builder\Field\TwigField;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid): void {
    $grid->addGrid(GridBuilder::create('app_user', '%app.model.user.class%')
        ->withFields(
            TwigField::create('name', '@Grid/Column/_prettyName.html.twig')
                ->setLabel('app.ui.name')
                ->setPath('.') // sets the field path to the root object
        )
    )
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

This allows you to render multiple properties inside the same field Twig template.

```twig
<strong>{{ data.name }}</strong>
<p>{{ data.description|markdown }}</p>
```

{% hint style="warning" %}
If you want to call the `setOptions` function, you must pass `'template'` as an argument again. Otherwise, it will be unset.

```php
$field->setOptions([
    'template' => ':Grid/Column:_prettyName.html.twig',

    // Your options here
]);
```

{% endhint %}

## Creating a custom Field Type

There are certain cases when built-in field types are not enough. Sylius Grids make it easy to define new types.

All you need to do is create your own class implementing **FieldTypeInterface** and register it as a service.

{% code title="src/Grid/FieldType/CustomType.php" lineNumbers="true" %}

```php
<?php

namespace App\Grid\FieldType;

use Sylius\Component\Grid\Attribute\AsField;
use Sylius\Component\Grid\Definition\Field;
use Sylius\Component\Grid\FieldTypes\FieldTypeInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

#[AsField(
    type: 'custom', // optional - FQCN by default
)]
final class CustomType implements FieldTypeInterface
{
    public function render(Field $field, $data, array $options = [])
    {
        // Your rendering logic... Use Twig, PHP or even external api...
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver
            ->setDefaults([
                'dynamic' => false
            ])
            ->setAllowedTypes([
                'dynamic' => ['boolean']
            ])
        ;
    }
}
```

{% endcode %}

That is all.

Now you can use your new column type in the grid configuration!

{% tabs %}
{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_admin_supplier:
            driver:
                name: doctrine/orm
                options:
                    class: App\Entity\Supplier
            fields:
                name:
                    type: custom
                    label: sylius.ui.name
```

{% endcode %}
{% endtab %}

{% tab title="PHP" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use App\Entity\Supplier;
use Sylius\Bundle\GridBundle\Builder\Action\Action;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Builder\Field\Field;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid) {
    $grid->addGrid(GridBuilder::create('app_admin_supplier', Supplier::class)
        ->withFields(
            Field::create('name', 'custom')
                ->setLabel('sylius.ui.name')
        )
    )
};
```

{% endcode %}

OR

{% code title="src/Grid/AdminSupplierGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\Supplier;
use Sylius\Bundle\GridBundle\Builder\Field\StringField;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Bundle\GridBundle\Grid\ResourceAwareGridInterface;

final class AdminSupplierGrid extends AbstractGrid implements ResourceAwareGridInterface
{
    public static function getName(): string
    {
           return 'app_admin_supplier';
    }

    public function buildGrid(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFields(
                Field::create('name', 'custom')
                    ->setLabel('sylius.ui.name')
            )
        ;
    }

    public function getResourceClass(): string
    {
        return Supplier::class;
    }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}


# Filters

*Filters* on grids act as predefined search options for each grid. Having a grid of objects you can filter out only those with a specified name, or value etc. Here you can find the supported filters. Keep in mind you can very easily define your own ones!

## String

Simplest filter type. It can filter by one or multiple fields.

**Filter by one field**

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/UserGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\User;
use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
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(
    resourceClass: User::class,
    name: 'app_user',
)]
final class UserGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFilters(
                Filter::create('username', 'string'),
                Filter::create('email', 'string'),
                Filter::create('firstName', 'string'),
                Filter::create('lastName', 'string'),
            )
            
            // can be simplified using StringFilter
            ->withFilters(
                StringFilter::create('username'),
                StringFilter::create('email'),
                StringFilter::create('firstName'),
                StringFilter::create('lastName'),
            )
        ;    
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_user:
            filters:
                username:
                    type: string
                email:
                    type: string
                firstName:
                    type: string
                lastName:
                    type: string
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
use Sylius\Bundle\GridBundle\Builder\Filter\StringFilter;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid): void {
    $grid->addGrid(GridBuilder::create('app_user', '%app.model.user.class%')
        ->withFilters(
            Filter::create('username', 'string'),
            Filter::create('email', 'string'),
            Filter::create('firstName', 'string'),
            Filter::create('lastName', 'string'),
        )
    
        // can be simplified using StringFilter
        ->withFilters(
            StringFilter::create('username'),
            StringFilter::create('email'),
            StringFilter::create('firstName'),
            StringFilter::create('lastName'),
        )
    )
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

**Filter by multiple fields**

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/UserGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\User;
use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
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(
    resourceClass: User::class,
    name: 'app_user',
)]
final class UserGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFilters(
                Filter::create('username', 'string')
                    ->setOptions(['fields' => ['username', 'email', 'firstName', 'lastName']])
            )
            
            // can be simplified using StringFilter
            ->withFilters(
                StringFilter::create('username', ['username', 'email', 'firstName', 'lastName'])
            )
        ;    
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_user:
            filters:
                search:
                    type: string
                    options:
                        fields: [username, email, firstName, lastName]
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
use Sylius\Bundle\GridBundle\Builder\Filter\StringFilter;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid): void {
    $grid->addGrid(GridBuilder::create('app_user', '%app.model.user.class%')
        ->withFilters(
            Filter::create('username', 'string')
                ->setOptions(['fields' => ['username', 'email', 'firstName', 'lastName']])
        )
    
        // can be simplified using StringFilter
        ->withFilters(
            StringFilter::create('username', ['username', 'email', 'firstName', 'lastName'])
        )
    )
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

**Search options**

This filter allows the user to select the following search options:

* contains
* not contains
* equal
* not equal
* starts with
* ends with
* empty
* not empty
* in
* not in
* member of

If you don't want to display all these matching possibilities, you can choose just one of them. Then only the input field will be displayed. You can achieve it like this:

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/UserGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\User;
use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
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(
    resourceClass: User::class,
    name: 'app_user',
)]
final class UserGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFilters(
                Filter::create('username', 'string')
                    ->setFormOptions([
                        'type' => 'contains',
                    ])
            )
            
            // can be simplified using StringFilter
            ->withFilters(
                StringFilter::create('username', null, 'contains')
            )
        ;    
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_user:
            filters:
                username:
                    type: string
                    form_options:
                        type: contains
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
use Sylius\Bundle\GridBundle\Builder\Filter\StringFilter;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid): void {
    $grid->addGrid(GridBuilder::create('app_user', '%app.model.user.class%')
        ->withFilters(
            Filter::create('username', 'string')
                ->setFormOptions([
                    'type' => 'contains',
                ])
        )
        
        // can be simplified using StringFilter
        ->withFilters(
            StringFilter::create('username', null, 'contains')
        )
    )
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

By configuring the filter as shown above, you will create an input field that filters user objects based on whether their username `contains` a given string.

## Boolean

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/UserGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\User;
use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
use Sylius\Bundle\GridBundle\Builder\Filter\BooleanFilter;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    resourceClass: User::class,
    name: 'app_user',
)]
final class UserGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFilters(
                Filter::create('enabled', 'boolean')
            )
            
            // can be simplified using BooleanFilter
            ->withFilters(
                BooleanFilter::create('enabled')
            )
        ;    
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_channel:
            filters:
                enabled:
                    type: boolean
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use Sylius\Bundle\GridBundle\Builder\Filter\BooleanFilter;
use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid): void {
    $grid->addGrid(GridBuilder::create('app_user', '%app.model.user.class%')
        ->withFilters(
            Filter::create('enabled', 'boolean')
        )
        
        // can be simplified using BooleanFilter
        ->withFilters(
            BooleanFilter::create('enabled')
        )
    )
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

This filter checks if a value is true or false.

## Date

This filter checks if a chosen datetime field is between given dates.

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/UserGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\User;
use Sylius\Bundle\GridBundle\Builder\Filter\DateFilter;
use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    resourceClass: User::class,
    name: 'app_user',
)]
final class UserGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFilters(
                Filter::create('createdAt', 'date'),
                Filter::create('completedAt', 'date'),
            )
            
            // can be simplified using DateFilter
            ->withFilters(
                DateFilter::create('createdAt'),
                DateFilter::create('completedAt'),
            )
        ;    
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_order:
            filters:
                createdAt:
                    type: date
                completedAt:
                    type: date
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use Sylius\Bundle\GridBundle\Builder\Filter\DateFilter;
use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid): void {
    $grid->addGrid(GridBuilder::create('app_user', '%app.model.user.class%')
        ->withFilters(
            Filter::create('createdAt', 'date'),
            Filter::create('completedAt', 'date'),
        )
        
        // can be simplified using DateFilter
        ->withFilters(
            DateFilter::create('createdAt'),
            DateFilter::create('completedAt'),
        )
    )
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Entity

This type filters by a chosen entity.

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/UserGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\User;
use Sylius\Bundle\GridBundle\Builder\Filter\EntityFilter;
use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    resourceClass: User::class,
    name: 'app_user',
)]
final class UserGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFilters(
                Filter::create('channel', 'entity')
                    ->setFormOptions(['class' => '%app.model.channel.class%']),
                Filter::create('customer', 'entity')
                    ->setFormOptions(['class' => '%app.model.customer.class%']),
            )
            
            // can be simplified using EntityFilter
            ->withFilters(
                EntityFilter::create('channel', '%app.model.channel.class%'),
                EntityFilter::create('customer', '%app.model.customer.class%'),
            )
        ;    
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_order:
            filters:
                channel:
                    type: entity
                    form_options:
                        class: "%app.model.channel.class%"
                        # You can pass any form options available in Entity Type
                        # See https://symfony.com/doc/current/reference/forms/types/entity.html
                        multiple: true 
                customer:
                    type: entity
                    form_options:
                        class: "%app.model.customer.class%"
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use Sylius\Bundle\GridBundle\Builder\Filter\EntityFilter;
use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid): void {
    $grid->addGrid(GridBuilder::create('app_user', '%app.model.user.class%')
        ->withFilters(
            Filter::create('channel', 'entity')
                ->setFormOptions([
                    'class' => '%app.model.channel.class%'
                    // You can pass any form options available in Entity Type
                    // See https://symfony.com/doc/current/reference/forms/types/entity.html
                    'multiple' => true,
                ]),
            Filter::create('customer', 'entity')
                ->setFormOptions(['class' => '%app.model.customer.class%']),    
        )
        
        // can be simplified using EntityFilter
        ->withFilters(
            EntityFilter::create('channel', '%app.model.channel.class%')
                ->addFormOption('multiple', true),
            EntityFilter::create('customer', '%app.model.customer.class%'),
        )
    )
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Money

This filter checks if an amount is within the specified range and is in the selected currency

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/UserGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\User;
use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
use Sylius\Bundle\GridBundle\Builder\Filter\MoneyFilter;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    resourceClass: User::class,
    name: 'app_user',
)]
final class UserGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFilters(
                Filter::create('total', 'money')
                    ->setFormOptions(['scale' => 3])
                    ->setOptions([
                        'currency_field' => 'currencyCode',
                        'scale' => 3,
                    ])
            )
            
            // can be simplified using MoneyFilter
            ->withFilters(
                MoneyFilter::create('total', 'currencyCode', 3)
            )
        ;    
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_order:
            filters:
                total:
                    type: money
                    form_options:
                        scale: 3
                    options:
                        currency_field: currencyCode
                        scale: 3
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use Sylius\Bundle\GridBundle\Builder\Filter\MoneyFilter;
use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid): void {
    $grid->addGrid(GridBuilder::create('app_user', '%app.model.user.class%')
        ->withFilters(
            Filter::create('total', 'money')
                ->setFormOptions(['scale' => 3])
                ->setOptions([
                    'currency_field' => 'currencyCode',
                    'scale' => 3,
                ])
        )
        
        // can be simplified using MoneyFilter
        ->withFilters(
            MoneyFilter::create('total', 'currencyCode', 3)
        )
    )
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

### *Warning*

Providing different `scale` values between *form\_options* and *options* may cause unwanted, and plausibly volatile results.

## Exists

This filter checks if the specified field contains any value

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/UserGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\User;
use Sylius\Bundle\GridBundle\Builder\Filter\ExistsFilter;
use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    resourceClass: User::class,
    name: 'app_user',
)]
final class UserGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFilters(
                Filter::create('date', 'exists')
                    ->setOptions(['field' => 'completedAt'])
            )
            
            // can be simplified using ExistsFilter
            ->withFilters(
                ExistsFilter::create('date', 'completedAt')
            )
        ;    
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_order:
            filters:
                date:
                    type: exists
                    options:
                        field: completedAt
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use Sylius\Bundle\GridBundle\Builder\Filter\ExistsFilter;
use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid): void {
    $grid->addGrid(GridBuilder::create('app_user', '%app.model.user.class%')
        ->withFilters(
            Filter::create('date', 'exists')
                ->setOptions(['field' => 'completedAt'])
        )
        
        // can be simplified using ExistsFilter
        ->withFilters(
            ExistsFilter::create('date', 'completedAt')
        )
    )
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Select

This type filters by a value chosen from the defined list

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/UserGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\User;
use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
use Sylius\Bundle\GridBundle\Builder\Filter\SelectFilter;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    resourceClass: User::class,
    name: 'app_user',
)]
final class UserGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFilters(
                Filter::create('state', 'select')
                    ->setFormOptions([
                        'choices' => [
                            'sylius.ui.ready' => 'Ready',
                            'sylius.ui.shipped' => 'Shipped',
                        ],
                    ])
            )
            
            // can be simplified using SelectFilter
            ->withFilters(
                SelectFilter::create('state', [
                    'sylius.ui.ready' => 'Ready',
                    'sylius.ui.shipped' => 'Shipped',
                ])
            )
        ;    
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_order:
            filters:
                state:
                    type: select
                    form_options:
                        choices:
                            sylius.ui.ready: Ready
                            sylius.ui.shipped: Shipped
```

{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
use Sylius\Bundle\GridBundle\Builder\Filter\SelectFilter;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid): void {
    $grid->addGrid(GridBuilder::create('app_user', '%app.model.user.class%')
        ->withFilters(
            Filter::create('state', 'select')
                ->setFormOptions([
                    'choices' => [
                        'sylius.ui.ready' => 'Ready',
                        'sylius.ui.shipped' => 'Shipped',
                    ],
                ])
        )
        
        // can be simplified using SelectFilter
        ->withFilters(
            SelectFilter::create('state', [
                'sylius.ui.ready' => 'Ready',
                'sylius.ui.shipped' => 'Shipped',
            ])
        )
    )
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Creating a custom Filter

Sylius Grids come with built-in filters, but there are use-cases where you need something more than a basic filter. Grids let you define your own filter types!

To add a new filter, we need to create an appropriate class and form type.

{% tabs %}
{% tab title="Using attributes (recommended)" %}
{% code title="src/Grid/Filter/SuppliersStatisticsFilter.php" lineNumbers="true" %}

```php
<?php
 
declare(strict_types=1);
 
namespace App\Grid\Filter;

use App\Form\Type\Filter\SuppliersStatisticsFilterType;
use Sylius\Bundle\GridBundle\Doctrine\DataSourceInterface;
use Sylius\Component\Grid\Attribute\AsFilter;
use Sylius\Component\Grid\Filtering\FilterInterface;
  
#[AsFilter(
    formType: SuppliersStatisticsFilterType::class,  // (custom) Symfony FormType
    template: '@SyliusBootstrapAdminUi/shared/grid/filter/select.html.twig',  // or you can use your own Twig template
    type: 'suppliers_statistics',  // optional - FQCN by default
)]
class SuppliersStatisticsFilter implements FilterInterface
{
    public function apply(DataSourceInterface $dataSource, $name, $data, array $options = []): void
    {
        // Your filtering logic.
        // $data['stats'] contains the submitted value!
        $queryBuilder = $dataSource->getQueryBuilder();
        $queryBuilder
            ->andWhere('stats = :stats')
            ->setParameter(':stats', $data['stats'])
        ;
    
        // You can leverage the ExpressionBuilder to apply driver-agnostic filters to the data source.
        // Combined with restrict(), it provides query builder–style functionalities for grid filters.
        $dataSource->restrict($dataSource->getExpressionBuilder()->equals('stats', $data['stats']));
    }
}
```

{% endcode %}

And the form type:

{% code title="src/Form/Type/Filter/SuppliersStatisticsFilterType.php" lineNumbers="true" %}

```php
<?php

namespace App\Form\Type\Filter;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class SuppliersStatisticsFilterType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder->add(
            'stats',
            ChoiceType::class,
            ['choices' => range($options['range'][0], $options['range'][1])]
        );
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver
            ->setDefaults([
                'range' => [0, 10],
            ])
            ->setAllowedTypes('range', ['array'])
        ;
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="Using ConfigurableFilterInterface (legacy way)" %}
{% code title="src/Grid/Filter/SuppliersStatisticsFilter.php" lineNumbers="true" %}

```php
<?php

namespace App\Grid\Filter;

use App\Form\Type\Filter\SuppliersStatisticsFilterType;
use Sylius\Bundle\GridBundle\Doctrine\DataSourceInterface;
use Sylius\Component\Grid\Filtering\ConfigurableFilterInterface;

class SuppliersStatisticsFilter implements ConfigurableFilterInterface
{
    public function apply(DataSourceInterface $dataSource, $name, $data, array $options = []): void
    {
        // Your filtering logic.
        // $data['stats'] contains the submitted value!
        $queryBuilder = $dataSource->getQueryBuilder();
        $queryBuilder
            ->andWhere('stats = :stats')
            ->setParameter(':stats', $data['stats'])
        ;
    
        // For driver abstraction you can use the expression builder. ExpressionBuilder is a kind of query builder.
        $dataSource->restrict($dataSource->getExpressionBuilder()->equals('stats', $data['stats']));
    }
    
    public static function getType() : string
    {
        return 'suppliers_statistics';
    }
    
    public static function getFormType() : string
    {
        return SuppliersStatisticsFilterType::class;
    }
}
```

{% endcode %}

And the form type:

{% code title="src/Grid/Filter/SuppliersStatisticsFilterType.php" lineNumbers="true" %}

```php
<?php

namespace App\Form\Type\Filter;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class SuppliersStatisticsFilterType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder->add(
            'stats',
            ChoiceType::class,
            ['choices' => range($options['range'][0], $options['range'][1])]
        );
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver
            ->setDefaults([
                'range' => [0, 10],
            ])
            ->setAllowedTypes('range', ['array'])
        ;
    }
}
```

{% endcode %}

Create a template for the filter, similar to the existing ones:

{% code title="templates/grid/filter/suppliers\_statistics.html.twig" lineNumbers="true" %}

```twig

<div data-gb-custom-block data-tag="form_theme" data-0='@SyliusUi/Form/theme.html.twig'></div>

{{ form_row(form) }}
```

{% endcode %}

If you use autoconfiguration, the filter is automatically registered as a grid filter.

But if you don't use autoconfiguration, let's register your new filter type as a service.

{% code title="config/services.yaml" lineNumbers="true" %}

```yaml
services:
    App\Grid\Filter\SuppliersStatisticsFilter:
        tags: ['sylius.grid_filter']
```

{% endcode %}
{% endtab %}
{% endtabs %}

Now you can use your new filter type in any grid configuration!

{% tabs %}
{% tab title="PHP" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use App\Entity\Tournament;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid) {
    $grid->addGrid(GridBuilder::create('app_tournament', Tournament::class)
        ->withFilters(
            Filter::create('stats', 'suppliers_statistics')
                ->setFormOptions(['range' => [0, 100]])
        )
    )
};
```

{% endcode %}

OR

{% code title="src/Grid/TournamentGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\Tournament;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;

#[AsGrid(
     name: 'app_tournament',
     resourceClass: Tournament::class,
)]
final class TournamentGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFilters(
                Filter::create('stats', 'suppliers_statistics')
                    ->setFormOptions(['range' => [0, 100]])
            )
        ;    
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_tournament:
            driver: doctrine/orm
            resource: app.tournament
            filters:
                stats:
                    type: suppliers_statistics
                    form_options:
                        range: [0, 100]
    
    templates:  # only needed if you didn't use AsFilter attribute
        filter:
            suppliers_statistics: '@App/Grid/Filter/suppliers_statistics.html.twig'
```

{% endcode %}
{% endtab %}
{% endtabs %}


# Actions

## Action groups

<div data-full-width="false"><figure><img src="/files/ePI5fYolaSGR4miq9Bxt" alt="Action groups"><figcaption></figcaption></figure></div>

Actions are classified into four types:

* main
* item
* subitem
* bulk

## Built-in actions

The grid package provides the following built-in actions:

| Name              | Usage      |
| ----------------- | ---------- |
| create            | main       |
| update            | item, bulk |
| delete            | item, bulk |
| show              | item       |
| apply\_transition | item, bulk |

### Create

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\Speaker;
use Sylius\Bundle\GridBundle\Builder\Action\CreateAction;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    resourceClass: Speaker::class,
    name: 'app_speaker',
)]
final class SpeakerGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withMainActions(
                // Add the "create" action into the "main" action group
                CreateAction::create()
                    // Optional, you can configure this globally instead.
                    ->setTemplate('path/to/your/action/template.html.twig')
                ,
            )
        ;
    }
}
```

### Update

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\Speaker;
use Sylius\Bundle\GridBundle\Builder\Action\UpdateAction;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    resourceClass: Speaker::class,
    name: 'app_speaker',
)]
final class SpeakerGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withItemActions(
                // Add the "update" action into the "item" action group
                UpdateAction::create()
                    // Optional, you can configure this globally instead.
                    ->setTemplate('path/to/your/action/template.html.twig')
                ,
            )
        ;
    }
}
```

### Delete

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\Speaker;
use Sylius\Bundle\GridBundle\Builder\Action\DeleteAction;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    resourceClass: Speaker::class,
    name: 'app_speaker',
)]
final class SpeakerGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withItemActions(
                // Add the "delete" action into the "item" action group
                DeleteAction::create()
                    // Optional, you can configure this globally instead.
                    ->setTemplate('path/to/your/action/template.html.twig')
                ,
            )
            ->withBulkActions(
                // Add the "delete" action into the "bulk" action group
                DeleteAction::create()
                    // Optional, you can configure this globally instead.
                    ->setTemplate('path/to/your/action/template.html.twig')
                ,
            )
        ;
    }
}
```

### Show

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\Speaker;
use Sylius\Bundle\GridBundle\Builder\Action\ShowAction;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    resourceClass: Speaker::class,
    name: 'app_speaker',
)]
final class SpeakerGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withItemActions(
                // Add the "show" action into the "item" action group
                ShowAction::create()
                    // Optional, you can configure this globally instead.
                    ->setTemplate('path/to/your/action/template.html.twig')
                ,
            )
        ;
    }
}
```

## Configuring your action templates globally

```yaml
sylius_grid:
    templates:
        action:
            create: path/to/your/create.html.twig
            delete: path/to/your/delete.html.twig
            show: path/to/your/show.html.twig
            update: path/to/your/update.html.twig
        bulk_action:
            delete: path/to/your/delete.html.twig
```

{% hint style="info" %}
The BootstrapAdminUi package will configure that for you automatically.

[BootstrapAdminUi configuration files](https://github.com/Sylius/Stack/blob/main/src/BootstrapAdminUi/config/app/grid/templates.php)
{% endhint %}

## Creating a custom Action

There are certain cases when built-in action types are not enough.

All you need to do is create your own action template and register it for the `sylius_grid`.

In this example, we will specify the action button's icon to be `mail` and its colour to be `purple` inside the template.

{% code title="@App/Grid/Action/contactSupplier.html.twig" lineNumbers="true" %}

```twig
{% import '@SyliusUi/Macro/buttons.html.twig' as buttons %}

{% set path = options.link.url|default(path(options.link.route, options.link.parameters)) %}

{{ buttons.default(path, action.label, null, 'mail', 'purple') }}
```

{% endcode %}

Now configure the new action's template like below in `config/packages/sylius_grid.yaml`:

{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    templates:
        action:
            contactSupplier: "@App/Grid/Action/contactSupplier.html.twig"
```

{% endcode %}

From now on, you can use your new action type in the grid configuration!

Let's assume that you already have a route for contacting your suppliers, then you can configure the grid action:

{% tabs %}
{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_admin_supplier:
            driver:
                name: doctrine/orm
                options:
                    class: App\Entity\Supplier
            actions:
                item:
                    contactSupplier:
                        type: contactSupplier
                        label: Contact Supplier
                        options:
                            link:
                                route: app_admin_contact_supplier
                                parameters:
                                    id: resource.id
```

{% endcode %}
{% endtab %}

{% tab title="PHP" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use App\Entity\Supplier;
use Sylius\Bundle\GridBundle\Builder\Action\Action;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid): void {
    $grid->addGrid(GridBuilder::create('app_admin_supplier', Supplier::class)
        ->withItemActions(
            Action::create('contactSupplier', 'contactSupplier')
                ->setLabel('Contact Supplier')
                ->setOptions([
                    'link' => [
                        'route' => 'app_admin_contact_supplier',
                        'parameters' => [
                            'id' => 'resource.id',
                        ],
                    ],
                ]),
        ])
    )
};
```

{% endcode %}

OR

{% code title="src/Grid/AdminSupplierGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\Supplier;
use Sylius\Bundle\GridBundle\Builder\Action\Action;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Bundle\GridBundle\Grid\ResourceAwareGridInterface;

final class AdminSupplierGrid extends AbstractGrid implements ResourceAwareGridInterface
{
    public static function getName(): string
    {
           return 'app_admin_supplier';
    }

    public function buildGrid(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withItemActions(
                Action::create('contactSupplier', 'contactSupplier')
                    ->setLabel('Contact Supplier')
                    ->setOptions([
                        'link' => [
                            'route' => 'app_admin_contact_supplier',
                            'parameters' => [
                                'id' => 'resource.id',
                            ],
                        ],
                    ]),
            ])
        ;    
    }
    
    public function getResourceClass(): string
    {
        return Supplier::class;
    }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Creating a custom Bulk Action

In some cases, forcing the user to click a button for each item in a grid isn't practical. Fortunately, you can take advantage of built-in bulk actions. However, these may not always be sufficient and might need customization.

To do this, simply create your own bulk action template and register it inside the `sylius_grid`.

In the template we will specify the button's icon to be `export` and its colour to be `orange`.

{% code title="@App/Grid/BulkAction/export.html.twig" %}

```twig
{% import '@SyliusUi/Macro/buttons.html.twig' as buttons %}

{% set path = options.link.url|default(path(options.link.route)) %}

{{ buttons.default(path, action.label, null, 'export', 'orange') }}
```

{% endcode %}

Now configure the new action's template:

{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    templates:
        bulk_action:
            export: "@App/Grid/BulkAction/export.html.twig"
```

{% endcode %}

From now on, you can use your new bulk action type in the grid configuration!

Let's assume that you already have a route for exporting by injecting ids. Now, you can configure the grid action:

{% tabs %}
{% tab title="YAML" %}
{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        app_admin_product:
            # ...
            actions:
                bulk:
                    export:
                        type: export
                        label: Export Data
                        options:
                            link:
                                route: app_admin_product_export
                                parameters:
                                    format: csv
```

{% endcode %}
{% endtab %}

{% tab title="PHP" %}
{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use App\Entity\Product;
use Sylius\Bundle\GridBundle\Builder\Action\Action;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Builder\Field\Field;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid) {
    $grid->addGrid(GridBuilder::create('app_admin_product', Product::class)
        ->withBulkActions(
            Action::create('export', 'export')
                ->setLabel('Export Data')
                ->setOptions([
                    'link' => [
                        'route' => 'app_admin_product_export',
                        'parameters' => [
                            'format' => 'csv',
                        ],
                    ]
                ]),
        )
    )
};
```

{% endcode %}

OR

{% code title="src/Grid/AdminProductGrid.php" lineNumbers="true" %}

```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\Product;
use Sylius\Bundle\GridBundle\Builder\Action\Action;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Bundle\GridBundle\Grid\ResourceAwareGridInterface;

final class AdminProductGrid extends AbstractGrid implements ResourceAwareGridInterface
{
    public static function getName(): string
    {
           return 'app_admin_product';
    }

    public function buildGrid(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withBulkActions(
                Action::create('export', 'export')
                    ->setLabel('Export Data')
                    ->setOptions([
                        'link' => [
                            'route' => 'app_admin_product_export',
                            'parameters' => [
                                'format' => 'csv',
                            ],
                        ]
                    ]),
            )
        ;    
    }
    
    public function getResourceClass(): string
    {
        return Product::class;
    }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}


# Mutators

When using packages that provide built-in grids (such as Sylius E-Commerce or Sylius plugins), you may need to customize their configuration.

Grid mutators allow you to modify an existing grid without overriding its original definition.

## Usage

Let's assume the `app_book` grid already contains a `title` field. We want the grid to be sorted by title.

To achieve this, create the following grid mutator:

```php
<?php

namespace App\Grid\Mutator;

use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Component\Grid\Attribute\AsGridMutator;
use Sylius\Component\Grid\Mutator\GridMutatorInterface;

#[AsGridMutator(grid: 'app_book')]
final class SortByTitleBookGridMutator implements GridMutatorInterface
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder->orderBy('title', 'asc');
    }
}
```

## Priorities

If multiple mutators target the same grid, you can control the order in which they are executed by using the `priority` option.

Mutators with a higher priority are executed before those with a lower priority.

```diff
<?php

namespace App\Grid\Mutator;

use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Component\Grid\Attribute\AsGridMutator;
use Sylius\Component\Grid\Mutator\GridMutatorInterface;

#[AsGridMutator(
    grid: 'app_book',
+    priority: 20,
)]
final class SortByTitleBookGridMutator implements GridMutatorInterface
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder->orderBy('title', 'asc');
    }
}
```


# Advanced configuration

By default, Doctrine options `fetchJoinCollection` and `useOutputWalkers` are enabled in all grids, but you can simply disable them with this config:

<details>

<summary>Yaml</summary>

{% code title="config/packages/sylius\_grid.yaml" lineNumbers="true" %}

```yaml
sylius_grid:
    grids:
        foo:
            driver:
                options:
                    pagination:                
                        fetch_join_collection: false
                        use_output_walkers: false
```

{% endcode %}

</details>

<details>

<summary>PHP</summary>

{% code title="config/packages/sylius\_grid.php" lineNumbers="true" %}

```php
<?php

use Sylius\Bundle\GridBundle\Builder\Field\StringField;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid): void {
    $grid->addGrid(GridBuilder::create('app_user', '%app.model.user.class%')
        ->setDriverOption('pagination', [
            'fetch_join_collection' => false,
            'use_output_walkers' => false,
        ])
    )
};
```

{% endcode %}

</details>

These changes may be necessary when you work with huge databases.


# Configuration Reference

This section provides all the configuration options for `sylius_grid`.

<details open>

<summary>Yaml</summary>

```yaml
sylius_grid:
    grids:
        app_user: # Your grid name
            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: twig # Type of field
                    label: Name # Label
                    path: . # dot means a whole object
                    sortable: ~ | field path
                    position: 100
                    options:
                        template: "@Grid/Column/_name.html.twig" # Only twig column
                        vars:
                            labels: # a template of how does the label look like
                    enabled: true
            filters:
                name:
                    type: string # Type of filter
                    label: app.ui.name
                    enabled: true
                    template: ~
                    position: 100
                    options:
                        fields: { }
                    form_options:
                        type: contains # type of string filtering option, if you only want to have one
                    default_value: ~
                enabled:
                    type: boolean # Type of filter
                    label: app.ui.enabled
                    enabled: true
                    template: ~
                    position: 100
                    options:
                        field: enabled
                    form_options: { }
                    default_value: ~
                date:
                    type: date # Type of filter
                    label: app.ui.created_at
                    enabled: true
                    template: ~
                    position: 100
                    options:
                        field: createdAt
                    form_options: { }
                    default_value: ~
                channel:
                    type: entity # Type of filter
                    label: app.ui.channel
                    enabled: true
                    template: ~
                    position: 100
                    options:
                        fields: [channel]
                    form_options:
                        class: "%app.model.channel.class%"
                    default_value: ~
            actions:
                main:
                    create:
                        type: create
                        label: sylius.ui.create
                        enabled: true
                        icon: ~
                        position: 100
                item:
                    update:
                        type: update
                        label: sylius.ui.edit
                        enabled: true
                        icon: ~
                        position: 100
                        options: { }
                    delete:
                        type: delete
                        label: sylius.ui.delete
                        enabled: true
                        icon: ~
                        position: 100
                        options: { }
                    show:
                        type: show
                        label: sylius.ui.show
                        enabled: true
                        icon: ~
                        position: 100
                        options:
                            link:
                                route: app_user_show
                                parameters:
                                    id: resource.id
                    archive:
                        type: archive
                        label: sylius.ui.archive
                        enabled: true
                        icon: ~
                        position: 100
                        options:
                            restore_label: sylius.ui.restore
                bulk:
                    delete:
                        type: delete
                        label: sylius.ui.delete
                        enabled: true
                        icon: ~
                        position: 100
                        options: { }
                subitem:
                    addresses:
                        type: links
                        label: sylius.ui.manage_addresses
                        options:
                            icon: cubes
                            links:
                                index:
                                    label: sylius.ui.list_addresses
                                    icon: list
                                    route: app_admin_user_address_index
                                    visible: resource.hasAddress
                                    parameters:
                                        userId: resource.id
                                create:
                                    label: sylius.ui.generate
                                    icon: random
                                    route: app_admin_user_address_create
                                    parameters:
                                        userId: resource.id
```

</details>

<details open>

<summary>PHP</summary>

```php
<?php

use Sylius\Bundle\GridBundle\Builder\Action\Action;
use Sylius\Bundle\GridBundle\Builder\Field\Field;
use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid): void {
    $grid->addGrid(GridBuilder::create('app_user', '%app.model.user.class%')
        ->setDriver('doctrine/orm')
        ->setRepositoryMethod('myCustomMethod', ['id' => 'resource.id'])
        ->orderBy('name', 'asc')
        ->setLimits([10, 25, 50, 100])
        ->withFields(
            Field::create('name', 'twig') // Name & Type of field
                ->setLabel('Name') // # Label
                ->setPath('.') // dot means a whole object
                ->setSortable(true)
                ->setPosition(100)
                ->setOptions([
                    'template' => '@Grid/Column/_name.html.twig', // Only twig column
                ])
                ->setEnabled(true)
        )
        ->withFilters(
            Filter::create('name', 'string') // Name & Type of filter
                ->setLabel('app.ui.name')
                ->setEnabled(true)
                ->setOptions(['fields' => []])
                ->setFormOptions(['type' => 'contains']) // type of string filtering option, if you only want to have one
        )
        ->withMainActions(
            Action::create('create', 'create')
                ->setLabel('sylius.ui.create')
                ->setEnabled(true)
                ->setIcon('plus')
                ->setPosition(100)
                ->setOptions([]),
        )
        ->withItemActions(
            Action::create('update', 'update')
                ->setLabel('sylius.ui.edit')
                ->setEnabled(true)
                ->setIcon('pencil')
                ->setPosition(100)
                ->setOptions([]),
            Action::create('delete', 'delete')
                ->setLabel('sylius.ui.delete')
                ->setEnabled(true)
                ->setIcon('trash')
                ->setPosition(100)
                ->setOptions([]),
            Action::create('show', 'show')
                ->setLabel('sylius.ui.show')
                ->setEnabled(true)
                ->setIcon('search')
                ->setPosition(100)
                ->setOptions([
                    'link' => [
                        'route' => 'app_user_show',
                        'parameters' => [
                            'id' => 'resource.id',
                        ],          
                    ],          
                ]),
            Action::create('archive', 'archive')
                ->setLabel('sylius.ui.archive')
                ->setEnabled(true)
                ->setIcon('search')
                ->setPosition(100)
                ->setOptions([
                    'restore_label' => 'sylius.ui.restore',          
                ]),
        )
        ->withBulkActions(
            Action::create('delete', 'delete')
                ->setLabel('sylius.ui.delete')
                ->setEnabled(true)
                ->setIcon('trash')
                ->setPosition(100)
                ->setOptions([]),
        )
        ->withSubItemActions(
            Action::create('addresses', 'links')
                ->setLabel('sylius.ui.manage_addresses')
                ->setOptions([
                    'icon' => 'cubes',
                    'links' => [
                        'index' => [
                            'label' => 'sylius.ui.list_addresses',
                            'icon' => 'list',
                            'route' => 'app_admin_user_address_index',
                            'visible' => 'resource.hasAddress',
                            'parameters' => [
                                'userId' => 'resource.id',
                            ],
                        ],
                        'create' => [
                            'label' => 'sylius.ui.generate',
                            'icon' => 'random',
                            'route' => 'app_admin_user_address_create',
                            'parameters' => [
                                'userId' => 'resource.id',
                            ],
                        ],
                    ],
                ]),
        )
    );
};
```

</details>


# Getting started

Admin UI contains minimalist generic templates and routes for your admin panels.

## Installation

Install the package using Composer and Symfony Flex:

```bash
composer require sylius/admin-ui
```

## Basic routes

* **Dashboard** - sylius\_admin\_ui\_dashboard
* **Login** - sylius\_admin\_ui\_login
* **LoginCheck** - sylius\_admin\_ui\_login\_check
* **Logout** - sylius\_admin\_ui\_logout

## Minimalist templates

All these following templates are kind of "empty".

You can install the optional [BootstrapAdminUi package](/bootstrap-admin-ui/getting-started) to configure their contents automatically.

### Crud templates

* crud/create.html.twig
* crud/index.html.twig
* crud/show\.html.twig
* crud/update.html.twig

*Usage with Sylius Resource package*

{% code title="src/Entity/Speaker.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    templatesDir: '@SyliusAdminUi/crud',
)]
class Speaker implements ResourceInterface
{
    // ...
}

```

{% endcode %}

### Dashboard

* dashboard/index.html.twig

### Login

* security/login.html.twig


# Getting started

Bootstrap Admin Ui lets you build your Bootstrap admin panels using Sylius and Symfony UX.

This package lets you set up the content of the [AdminUi package](/admin-ui/getting-started) templates.

## Installation

Install the package using Composer and Symfony Flex:

```bash
composer require sylius/bootstrap-admin-ui
```

## Configuring the CRUD templates

CRUD templates are split into configurable blocks.

You can add new blocks, disable existing ones, or reorder them using the [TwigHooks package](/twig-hooks/getting-started).

### Usage with the Sylius Resource package

{% code title="src/Entity/Speaker.php" lineNumbers="true" %}

```php
namespace App\Entity;

use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Model\ResourceInterface;

#[AsResource(
    // We still use the Sylius admin ui templates dir.
    templatesDir: '@SyliusAdminUi/crud', 
)]
class Speaker implements ResourceInterface
{
    // ...
}

```

{% endcode %}

### Create

This package sets up the template content needed to create a new resource.

This adds configurable blocks to the `@SyliusAdminUi/crud/create.html.twig` template.

**Overview of the blocks**

{% @mermaid/diagram content="flowchart LR
Template(Create template) --> Hook{Hook 'create'}

```
Hook --> Sidebar([Sidebar])
Hook --> Navbar([Navbar])
Hook --> Content([Content])

Content --> HookContent{Hook 'content'}

HookContent --> Flashes([Flashes])
HookContent --> Header([Header])
HookContent --> FormErrorAlert([Form Error Alert])
HookContent --> Form([Form])" %}
```

**Overview of the block templates**

{% @mermaid/diagram content="flowchart LR
Template\["@SyliusAdminUi/crud/create.html.twig"] --> Hook\["Hook: 'create'"]

```
Hook --> Sidebar["@SyliusBootstrapAdminUi/shared/crud/common/sidebar.html.twig"]
Hook --> Navbar["@SyliusBootstrapAdminUi/shared/crud/common/navbar.html.twig"]
Hook --> Content["@SyliusBootstrapAdminUi/shared/crud/common/content.html.twig"]" %}
```

### Index

This package sets up the template content needed to list resources.

This adds configurable blocks to the `@SyliusAdminUi/crud/index.html.twig` template.

**Overview of the blocks**

{% @mermaid/diagram content="flowchart LR
Template(Index template) --> Hook{Hook 'index'}

```
Hook --> Sidebar([Sidebar])
Hook --> Navbar([Navbar])
Hook --> Content([Content])

Content --> HookContent{Hook 'content'}

HookContent --> Flashes([Flashes])
HookContent --> Header([Header])
HookContent --> Grid([Grid])" %}
```

### Show

This package sets up the template content needed to show resource details.

This adds configurable blocks to the `@SyliusAdminUi/crud/show.html.twig` template.

**Overview of the blocks**

{% @mermaid/diagram content="flowchart LR
Template(Show template) --> Hook{Hook 'show'}

```
Hook --> Sidebar([Sidebar])
Hook --> Navbar([Navbar])
Hook --> Content([Content])

Content --> HookContent{Hook 'content'}

HookContent --> Flashes([Flashes])
HookContent --> Header([Header])" %}
```


# Getting started

Twig Extra is a set of Twig extensions that provides additional Twig helpers.

## Installation

Install the package using Composer and Symfony Flex:

```bash
composer require sylius/twig-extra
```

## Features

### Sort by

This extension allows you to sort an array of objects by a specific property.

```php
class Book {
    public function __construct() {
        public string $name,
    }
}

$books = [
    new Book('The Shining'), 
    new Book('The Lord Of The Rings'), 
    new Book('Dune'),
    new Book('Wuthering Heights'),
    new Book('Fahrenheit 451'),
];
```

```twig
{% raw %}
<ul>
{% for book in books|sylius_sort_by('name') %}
    <li>{{ book.name }}</li>
{% endif %}
</ul>
{% endraw %}
```

```
. Dune
. Fahrenheit 451
. The Lord Of The Rings
. The Shining
. Wuthering Heights
```

You can also sort nested arrays.

```php

$books = [
    ['name' => 'The Shining'], 
    ['name' => 'The Lord Of The Rings'],
    ['name' => 'Dune'],
    ['name' => 'Wuthering Heights'],
    ['name' => 'Fahrenheit 451'],
];
```

You just need to encapsulate the key with `[]`.

```twig
{% raw %}
<ul>
{% for book in books|sylius_sort_by('[name]') %}
    <li>{{ book.name }}</li>
{% endif %}
</ul>
{% endraw %}
```

### Test HTML attribute

This Twig extension lets you add data attributes in a test environment or when debug mode is enabled. This makes it easy to identify your data in E2E tests while minimizing dependency on HTML changes.

```twig
<h1 {{ sylius_test_html_attribute('title')>The Shining</h1>
```

```html
<h1 data-test-title>The Shining</h1>
```

### Test Form HTML attribute

Like the `sylius_test_html_attribute` Twig extension, this one allows you to add some data attributes in your test environment or when debug mode is enabled. This function adds the data attribute via the `attr` Twig variable on a form theme block.

```twig
{{ form_row(form.title, sylius_test_form_attribute('title')) }}
```

```html
<!-- Actual html output below depends on your form theme -->
<label for="book_title">Title</label>
<input 
        type="text" 
        id="book_title" 
        name="title" 
        data-test-title <!-- This is the added data attribute -->
/>
```


# Getting started

Twig Hooks are a robust and powerful alternative to the Sonata Block Events and the old Sylius Template Events systems.

### Main features

* built-in support for *Twig templates*, *Twig Components* and *Symfony Live Components*
* adjustability
* autoprefixing hooks
* configurable hookables
* priority mechanism
* easy enable/disable mechanism for each hook

### Installation

Install the package using Composer and Symfony Flex:

```bash
composer require sylius/twig-hooks
```

### Your first hook & hookable

Once Twig Hooks is installed, you can open **any** Twig file and define your first hook.

{% code title="some.html.twig" %}

```twig
{% hook 'my_first_hook' %}
```

{% endcode %}

This way, `my_first_hook` becomes a unique name which we can use to hook into that specific spot.

{% hint style="success" %}
The ideal hook name:

* is lowercase
* has its logical parts separated with dots (`.`)
* when there is more than one word, they are separated by underscores (`_`)

<mark style="color:green;">Recommended:</mark>

* `index`
* `index.sidebar`
* `index.top_menu`

<mark style="color:red;">Not recommended:</mark>

* index
* indextopmenu
  {% endhint %}

**Hooking into a hook**

For the purpose of this example, let's consider we want to render the `some_block.html.twig` template inside the `my_first_hook` hook. First step is to create a `twig_hooks.yaml` file (or any other format you use) under the `config/packages/` directory (if you don't have one already, of course).

Now, we can define our first hookable with the following configuration:

{% code title="config/packages/twig\_hooks.yaml" lineNumbers="true" %}

```yaml
sylius_twig_hooks:
    hooks:
        'my_first_hook':
            some_block:
                template: 'some_block.html.twig'
```

{% endcode %}

Decomposing the example above we can notice that:

1. `sylius_twig_hooks` is the main key for Twig Hooks configuration
2. `hooks` is a configuration key for defining hookables for all hooks
3. `my_first_hook` is our hook name, defined on the Twig file level
4. `some_block` is the name of our hookable, it can be any string, but it should be unique for a given hook unless you want to override the existing hookable (if you want to read more about overriding hookables check the overriding-hookables.md section)
5. finally we have a `template` key that defines which template should be rendered inside the `my_first_hook` hook

**Possible hookable configuration options**

Depending on the hookable template, we can pass different configuration options while defining hookables:

{% tabs %}
{% tab title="Hookable Template" %}
{% code lineNumbers="true" %}

```yaml
sylius_twig_hooks:
    hooks:
        'my_first_hook':
            some_block:
                template: 'some_block.html.twig'
                enabled: true # whether the hookable is enabled
                context: [] # key-value pair that will be passed to the context bag
                configuration: [] # key-value pair that will be passed to the configuration bag
                priority: 0 # priority, the higher the number, the earlier the hookable will be hooked
```

{% endcode %}
{% endtab %}

{% tab title="Hookable Component" %}
{% code title="" %}

```yaml
sylius_twig_hooks:
    hooks:
        'my_first_hook':
            some_block:
                component: 'app:block' # component key
                enabled: true # whether the hookable is enabled
                context: [] # key-value pair that will be passed to the context bag
                props: [] # key-value pair that will be passed to our component as props
                configuration: [] # key-value pair that will be passed to the configuration bag
                priority: 0 # priority, the higher the number, the earlier the hookable will be hooked
```

{% endcode %}
{% endtab %}
{% endtabs %}


# Passing data to your hookables

One of the most powerful aspects of hooks & hookables is the ability to pass data down to children elements. We can have two sources of context data:

* Hook-level defined data
* Hookable-level defined data

Context data from these two sources is merged and passed to the **hookable** template or component together with the metadata , so we can access them.

<div data-full-width="false"><figure><img src="/files/oWO3nJyX8BSddN9ERhLI" alt=""><figcaption></figcaption></figure></div>

### Example

| <p>Let's assume we want to render a form in our <code>index.html.twig</code> template via a <code>form</code> variable containing a <code>FormView</code> instance.</p><p>Here, we define an <strong><code>index.form</code></strong> hook, and we can pass it the form's context data thanks to the <code>with</code> keyword.</p> |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

This means that we can technically pass down multiple pieces of data to hookables that will hook into `index.form`.

{% code title="index.html.twig" lineNumbers="true" fullWidth="false" %}

```twig
<div class="container">
    {{ form_start(form) }}
    {{ form_errors(form }}
    {{ form_widget(form._token) }}
    
    {% hook 'index.form' with { form } %}
    
    {{ form_end(form, {render_rest: false} }}
</div>
```

{% endcode %}

{% hint style="info" %}
`with { form }` is a short-hand for `with { form: form }`, so the key for our `FormView` in the context data bag will be `form.`
{% endhint %}

Now let's create a Twig template that renders a field from our form and let's make it a hookable. We have 3 possible options to do this :&#x20;

{% tabs %}
{% tab title="property access via hookable\_metadata" %}

<pre class="language-twig" data-title="index/some_field.html.twig" data-line-numbers><code class="lang-twig">&#x3C;div class="field">
  {{ form_row(hookable_metadata.context.form.some_field) }}
<strong> &#x3C;/div>
</strong></code></pre>

{% endtab %}

{% tab title="variable binding" %}

<pre class="language-twig" data-title="index/some_field.html.twig" data-line-numbers><code class="lang-twig">&#x3C;div class="field">
  {% set context = hookable_metadata.context %}
  {{ form_row(context.form.some_field) }}
<strong> &#x3C;/div>
</strong></code></pre>

{% endtab %}

{% tab title="utility function" %}

<pre class="language-twig" data-title="index/some_field.html.twig" data-line-numbers><code class="lang-twig">&#x3C;div class="field">
  {% set context = get_hookable_context() %}
  {{ form_row(context.form.some_field) }}
<strong> &#x3C;/div>
</strong></code></pre>

{% endtab %}
{% endtabs %}

{% hint style="info" %}
You can access the context data in multiple ways, so you can pick the one you like the most. Available options are:

* getting it directly from the `hookable_metadata` object like `hookable_metadata.context.<data_key>`
* getting the context data bag via the Twig function like `get_hookable_context().<data_key>`
  {% endhint %}

### Override behavior

When the same context data key is defined at both the **hook** and **hookable** levels, the **hookable-level** value takes precedence.

{% hint style="info" %}
You can use this to override hook-level data by redefining the key at the hookable level.
{% endhint %}


# Making your hookables configurable

Sometimes when you are creating a bundle or a reusable template for different hookables, you might want to provide a way to adjust it to a given context. Thanks to the configuration data bag, you are able to achieve it easily.

While using a hookable template, you can access `configuration` keys via `hookable_metadata` Twig var using `hookable_metadata.configuration.<key_name>` or `get_hookable_configuration().<key_name>`.

#### Example

{% code title="index.html.twig" lineNumbers="true" %}

```twig
{#
 # we assume there is a `form` variable holding a `FormView` instance passed
 # from the controller
 #}

<div class="container">
    {{ form_start(form) }}
        {{ form_errors(form) }}
        {{ form_widget(form._token) }}
    
        {% raw %}
{% hook 'index.form' with { form } %}
{% endraw %}
    {{ form_end(form, {render_rest: false}) }}
</div>
```

{% endcode %}

{% code title="generic\_field.html.twig" lineNumbers="true" %}

```twig
<div class="{{ hookable_metadata.configuration.attr.class|default("field) }}">
     {{ form_row(
          hookable_metadata.context.form[hookable_metadata.configuration.field_name]
     ) }}
</div>
```

{% endcode %}

{% code title="twig\_hooks.yaml" lineNumbers="true" %}

```yaml
sylius_twig_hooks:
    hooks:
        'index.form':
            name:
                template: 'generic_field.html.twig'
                configuration:
                    field_name: 'name'
                    attr:
                        class: 'field special-field'
```

{% endcode %}


# Autoprefixing feature

## Autoprefixing feature

{% hint style="warning" %}
`Autoprefixing` is turned off by default. If you want to use this feature you need to set the `enable_autoprefixing` setting to `true` in your `config/packages/twig_hooks.yaml` file:

```yaml
sylius_twig_hooks:
    # ...
    enable_autoprefixing: true
    # ...
```

{% endhint %}

When you are creating a bundle, or a bigger project like [Sylius](https://sylius.com), you might want to rely fully on Twig Hooks to provide easy and flexible way of modifying and extending your views.

Enabling the autoprefixing feature might improve your developer experience. This feature is crucial for creating [Composable Layouts with a predictable structure](/twig-hooks/composable-layouts-with-a-predictable-structure).

{% hint style="info" %}
If you did not read the [Composable Layouts with a predictable structure](/twig-hooks/composable-layouts-with-a-predictable-structure) section we encourage you to do it before you read more about the autoprefixing feature.
{% endhint %}

The mechanism of autoprefixing is pretty simple. We check if there are any prefixes, then we iterate over them and prepend the hook name with a given prefix.

#### Defining prefixes

Prefixes by default are injected automatically, and they are the name of the hook where the hookable is rendered.

> As a developer I define the **index.form** hook in my template
>
> And I define the **some\_field** hookable in it
>
> So when I check prefixes **inside** the **some\_field** hookable I should get `index.form`

In case we deal with a complex hook:

> As a developer I define the **index.form, common.form** hook in my template
>
> And I define the **some\_field** hookable in **index.form**
>
> So when I check prefixes **inside** the **some\_field** hookable I should get `index.form` and `common.form`

If for some reason you want to take the control over the passed prefixes, you can override existing prefixes using the `_prefixes` magic variable when you are creating a hook inside a Twig template:

{% code title="index.html.twig" lineNumbers="true" %}

```twig
{% hook 'index.form' with {
    _prefixes: ['my_custom_prefix']
} %}
```

{% endcode %}

From now, only the value of `_prefixes` will be taken into account.

#### Example

{% code title="index.html.twig" lineNumbers="true" %}

```twig
{% hook 'app.index' %}

{# index.html.twig is an entry template, so it is not an hookable #}
```

{% endcode %}

<pre class="language-twig" data-title="index/content.html.twig" data-line-numbers><code class="lang-twig"><strong>{% hook 'content' %}
</strong>
{# this template is an hookable, and is hooked into app.index #}

{#
 # so {% hook 'content' %} this is a shorter form of {% hook 'app.index.content' %}
 # when autoprefixing is turned on
 #}
</code></pre>

{% code title="index/content/button.html.twig" lineNumbers="true" %}

```twig
<button>Click me!</button>
```

{% endcode %}

The configuration for the hooks and hookables above is:

{% code title="config/packages/twig\_hooks.yaml" lineNumbers="true" %}

```yaml
sylius_twig_hooks:
    hooks:
        'app.index':
            content:
                template: 'index/content.html.twig'

        'app.index.content':
            button:
                template: 'index/content/button.html.twig
```

{% endcode %}

{% hint style="info" %}
The structure of directories above does not matter, all templates can be on the same level of nesting. However, in this example we are following creating [Composable Layouts with a predictable structure](/twig-hooks/composable-layouts-with-a-predictable-structure) guide.
{% endhint %}


# Composable Layouts with a predictable structure

{% hint style="info" %}
All examples in this chapter assume you are familiar with the [Autoprefixing feature](/twig-hooks/autoprefixing-feature).
{% endhint %}

Before we dive into **how** to create composable layouts with `Twig Hooks`, let's first understand **what** the `composable` part means.

> **composable** (*not* [*comparable*](https://en.wiktionary.org/wiki/Appendix:Glossary#comparable))
>
> 1. Capable of being [composed](https://en.wiktionary.org/wiki/compose#English) (as from multiple [constituent](https://en.wiktionary.org/wiki/constituent#Adjective) or [component](https://en.wiktionary.org/wiki/component#Adjective) elements).
>
> source: [Wiktioniary](https://en.wiktionary.org/wiki/composable)

So, to achieve composability we need building blocks from which we will build our layouts. With Twig Hooks we can use the following building blocks (in Twig Hooks we call them hookables):

* A regular Twig template
* Twig Component
* Live Component

As mentioned in previous chapters, hookables can also define their own hooks, so we are able to create more complex building blocks like the header section which consisted of a title and some action buttons.

To fully utilize this functionality, make sure:

* you have turned on the [Autoprefixing feature](/twig-hooks/autoprefixing-feature) and you are familiar with it
* you are familiar with [Twig Components](https://symfony.com/bundles/ux-twig-component/current/index.html) and the concept of [anonymous components](https://symfony.com/bundles/ux-twig-component/current/index.html#anonymous-components)

### Predictable structure

The idea behind the `Predictable structure` is to organize your hookables (your Twig templates) to make it easier to guess the hooks names with which they are rendered, and to be able to find them based on a given hook name. This approach aims to reduce the need to browse through multiple folders when trying to locate a template we want to edit or check.

{% hint style="info" %}
When we define a hook in a template which is not a hookable (e.g., it is rendered by a controller) we call such a hook a **primal hook**. For primal hooks we always need to define a full hook name (as the [Autoprefixing feature](/twig-hooks/autoprefixing-feature) does not work in this case) and a context we want to pass to hookables.

The opposite of **primal hookable** is **subsequent hook**. This means that a given template is a hookable and defines a new hook (or hooks).
{% endhint %}

The main rule for predictable structure is:

> Given a template defines a hook, all its hookables should live in a directory with the same name as the template.

So, if `templates/course/create.html.twig` defines a hook, all its direct hookables should live in the `templates/course/create/` directory (e.g., `templates/course/create/form.html.twig`).

#### Example

To better understand this rule, let's consider the following directory structure (and let's assume `create.html.twig`, `form.html.twig` and `header.html.twig` define new hooks inside):

<figure><img src="/files/oiZpvYRROLhXntyxhDID" alt=""><figcaption></figcaption></figure>

we can tell that:

* `create.html.twig` has two direct hookables (`form` , `header`)
* `header.html.twig` has one direct hookable (`title` )
* `form.html.twig` has five direct hookables (`create` , `max_number_of_students`, `name`, `price`, `start_date`)

Moreover, assuming `create.html.twig` defines an `app.course.create` primal hook, we can tell that the `form.html.twig` defines an `app.course.create.form` subsequent hook and `header.html.twig` defines a `app.course.create.header` subsequent hook.

{% hint style="info" %}
`app.course.create` can be any name, as there is no convention enforcement for primal hooks. However, in this example we assume that every hook is prefixed with `app` and the rest of the hook name is related to the template path.

When defining hooks inside bundles, it is recommended to use a configuration key as a prefix. For instance, for `@SyliusAdmin/product/create/html.twig` we get `sylius_admin.product.create` as `SyliusAdminBundle` uses `sylius_admin` as a configuration key.
{% endhint %}


# Advanced


# Ergonomic work with hooks


# Metadata objects

Metadata objects have been introduce to structurize the information passed between hooks and hookables. In Twig Hooks we have two metadata objects:

* A hook metadata object, which contains information about the hook name, and the hook-level defined context
* A hookable metadata object, which contains information about the hook which rendered it, the merged context, the configuration and prefixes

### Accessing a hook metadata

A hook metadata object can be accessed only from a hookable metadata object.&#x20;

### Accessing a hookable metadata

A hookable metadata can be accessed from a Twig template that is a hookable. You can do this by:

* using the `hookable_metadata` variable which is automatically created for hookables
* using the `get_hookable_metadata()` function

There is no difference between these two methods, so you can pick the one you prefer the one that best fits your coding style.&#x20;

{% hint style="info" %}
There are also Twig functions which are shortcuts for accessing concrete data bags from a metadata object:

* `get_hookable_context()` for accessing the context
* `get_hookable_configuration` for accessing the configuration
  {% endhint %}


# Multiple hooks inside a single template


# Overriding hookables

While designing more complex systems, you might want to provide more than one hook name while defining a hook.

```twig
{% hook ['app.course.create', 'app.common.create'] %}
```

{% hint style="info" %}
While defining multiple hooks names, remember the earlier ones have higher priority than later ones. So, `header` hookable from `app.course.create` will override the one from `app.common.create`.
{% endhint %}

You can use this mechanism for creating set of hookables for "generic" use-cases, and override only specific hookables on a concrete pages. For example, every `create` page (or at least more of them) is similar. So we can define these all similar elements for the `app.common.create` hook, and override only specific ones with other hookables or configuration.

Moreover, there is no limit for number of hook names you can define. So, you can create the following hook:

```twig
{% hook ['app.course.create.form', 'app.common.create.form', 'app.common.component.form'] %}
```

A hookable **with the same name** from `app.common.create.form` will override a hookable with the same name from `app.common.component.form`. As same as a hookable with the same name from `app.course.create.form` will override a hookable with the same name from `app.common.create.form` and `app.common.component.form`.

Of course, this mechanism is scoped to a given hook definiton. In other template you can still define:

```twig
{% hook ['app.common.create.form', 'app.common.component.form'] %}
```

and in this case only hookables with the same name from `app.common.create.form` will override the ones from `app.common.component.form`.

### Example

Let's consider the following hooks configuration:

{% code title="config/packages/twig\_hooks.yaml" lineNumbers="true" %}

```yaml
sylius_twig_hooks:
    hooks:
        'app.common.create':
            header:
                template: 'common/create/header.html.twig'
            content:
                template: 'common/create/content.html.twig'
```

{% endcode %}

And let's assume that we have two pages:

* creating a course
* creating a course category

with the following templates:

{% code title="templates/course/create.html.twig" %}

```twig
{% hook ['app.course.create', 'app.common.create'] %}
```

{% endcode %}

{% code title="templates/course\_category/create.html.twig" %}

```twig
{% hook ['app.course_category.create', 'app.common.create'] %}
```

{% endcode %}

As there is no configuration for the `app.course.create` and `app.course_category.create` the `app.common.create` configuration will be applied. But, once we define the following configuration:

{% code title="config/packages/twig\_hooks.yaml" lineNumbers="true" %}

```yaml
sylius_twig_hooks:
    hooks:
        'app.common.create':
            header:
                template: 'common/create/header.html.twig'
            content:
                template: 'common/create/content.html.twig'

        'app.course.create':
            header:
                template: 'course/create/header.html.twig'
```

{% endcode %}

instead of the `common/create/header.html.twig` template we will see the `course/create/header.html.twig` template on the "Create a course" page.


