+
+Recommended namespace for the action type classes is `App\DataTable\Action\Type\`.
+
+
+
+### Action type inheritance
+
+Because our modal action fundamentally renders as a button, let's base it off the built-in [`ButtonActionType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/ButtonActionType.php).
+Provide the fully-qualified class name of the parent type in the `getParent()` method:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Action\Type\AbstractActionType;
+use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType;
+
+class ModalActionType extends AbstractActionType
+{
+ public function getParent(): ?string
+ {
+ return ButtonActionType::class;
+ }
+}
+```
+
+::: tip
+If you take a look at the [`AbstractActionType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/AbstractActionType.php),
+you'll see that `getParent()` method returns fully-qualified name of the [`ActionType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/ActionType.php) type class.
+This is the type that defines all the basic options, such as `attr`, `label`, etc.
+:::
+
+### Rendering the action type
+
+Because our modal action is based off the built-in [`ButtonActionType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/ButtonActionType.php),
+it will be rendered as a button without any additional configuration. However, in our case, we want to add the modal itself.
+
+First, create a custom theme for the data table, and create a `action_modal_value` block:
+
+```twig
+{# templates/data_table/theme.html.twig #}
+
+{% block action_modal_value %}
+
+
+
+ {# Bootstrap modal contents... #}
+
+{% endblock %}
+```
+
+The block naming follows a set of rules:
+
+- for actions, it always starts with `action_` prefix;
+- next comes the block prefix of the action type;
+- last part is always the `_value` suffix;
+
+If you take a look at the [`AbstractActionType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/AbstractActionType.php),
+you'll see that `getBlockPrefix()` returns snake cased short name of the type class, without the `ActionType` suffix.
+
+In our case, because the type class is named `ModalActionType`, the default block prefix equals `modal`. Simple as that.
+
+Now, the custom theme should be added to the bundle configuration:
+
+::: code-group
+
+```yaml [YAML]
+kreyu_data_table:
+ defaults:
+ themes:
+ # ...
+ - 'data_table/theme.html.twig'
+```
+
+```php [PHP]
+use Symfony\Config\KreyuDataTableConfig;
+
+return static function (KreyuDataTableConfig $config) {
+ $config->defaults()->themes([
+ // ...
+ 'data_table/theme.html.twig',
+ ]);
+};
+```
+
+:::
+
+If the `action_modal_value` block wasn't defined in any of the configured themes, the bundle will render block of the parent type.
+In our example, because we set [`ButtonActionType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/ButtonActionType.php) as a parent, a `action_button_value` block will be rendered.
+
+### Adding configuration options
+
+Action type options allow to configure the behavior of the action types.
+The options are defined in the `configureOptions()` method, using the [OptionsResolver component](https://symfony.com/doc/current/components/options_resolver.html).
+
+Imagine, that you want to provide a template to render as the action modal contents.
+The template could be provided by a custom `template_path` and `template_vars` options:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Action\Type\AbstractActionType;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class ModalActionType extends AbstractActionType
+{
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver
+ // define options required by the type
+ ->setRequired('template_path')
+ // define available options and their default values
+ ->setDefaults([
+ 'template_vars' => [],
+ ])
+ // optionally you can restrict type of the options
+ ->setAllowedTypes('template_path', 'string')
+ ->setAllowedTypes('template_vars', 'array')
+ ;
+ }
+}
+```
+
+Now you can configure the new option when using the action type:
+
+```php
+class UserDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ // ...
+ ->addRowAction('details', ModalActionType::class, [
+ 'template_path' => 'user/details.html.twig',
+ ])
+ ;
+ }
+}
+```
+
+### Passing variables to the template
+
+Now, the `template_path` and `template_vars` options are defined, but are not utilized by the system in any way.
+In our case, we'll pass the options to the view, and use them to render the template itself:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType;
+use Kreyu\Bundle\DataTableBundle\Action\ActionView;
+use Kreyu\Bundle\DataTableBundle\Action\ActionInterface;
+
+class ModalActionType extends ButtonActionType
+{
+ public function buildView(ActionView $view, ActionInterface $action, array $options): void
+ {
+ $view->vars['template_path'] = $options['template_path'];
+ $view->vars['template_vars'] = $options['template_vars'];
+ }
+}
+```
+
+Now we can update the template of the type class to use the newly added variables:
+
+```twig
+{# templates/data_table/theme.html.twig #}
+
+{% block action_modal_value %}
+
+
+
+{% endblock %}
+```
+
+### Using row data in options
+
+> What if I want to pass an option based on the row data?
+
+If the action type is used for a row action, the `ActionView` parent will be an instance of `ColumnValueView`,
+which can be used to retrieve a data of the row. This can be used in combination with accepting the `callable` options:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType;
+use Kreyu\Bundle\DataTableBundle\Action\ActionView;
+use Kreyu\Bundle\DataTableBundle\Action\ActionInterface;
+use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView; // [!code ++]
+
+class ModalActionType extends ButtonActionType
+{
+ public function buildView(ActionView $view, ActionInterface $action, array $options): void
+ {
+ if ($view->parent instanceof ColumnValueView) { // [!code ++]
+ $value = $view->parent->vars['value']; // [!code ++]
+
+ foreach (['template_path', 'template_vars'] as $optionName) { // [!code ++]
+ if (is_callable($templatePath)) { // [!code ++]
+ $options[$optionName] = $options[$optionName]($value); // [!code ++]
+ } // [!code ++]
+ } // [!code ++]
+ } // [!code ++]
+
+ $view->vars['template_path'] = $options['template_path'];
+ $view->vars['template_vars'] = $options['template_vars'];
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver
+ // define options required by the type
+ ->setRequired('template_path')
+ // define available options and their default values
+ ->setDefaults([
+ 'template_vars' => [],
+ ])
+ // optionally you can restrict type of the options
+ ->setAllowedTypes('template_path', 'string') // [!code --]
+ ->setAllowedTypes('template_path', ['string', 'callable']) // [!code ++]
+ ->setAllowedTypes('template_vars', 'array') // [!code --]
+ ->setAllowedTypes('template_vars', ['array', 'callable']) // [!code ++]
+ ;
+ }
+}
+```
+
+Now, you can use the `callable` options when defining the modal row action:
+
+```php
+class UserDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ // ...
+ ->addRowAction('details', ModalActionType::class, [
+ 'template_path' => 'user/details.html.twig',
+ 'template_vars' => function (User $user) { // [!code ++]
+ return [ // [!code ++]
+ 'user_id' => $user->getId(), // [!code ++]
+ ]; // [!code ++]
+ }, // [!code ++]
+ ])
+ ;
+ }
+}
+```
+
+## Action type extensions
+
+Action type extensions allows modifying configuration of the existing action types, even the built-in ones.
+Let's assume, that we want to add a [Bootstrap tooltip](https://getbootstrap.com/docs/5.3/components/tooltips/#overview) for every button action, as long as their `title` attribute is defined.
+
+Action type extensions are classes that implement [`ActionTypeExtensionInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Extension/ActionTypeExtensionInterface.php).
+However, it's better to extend from the [`AbstractActionTypeExtension`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Extension/AbstractActionTypeExtension.php):
+
+```php
+use Kreyu\Bundle\DataTableBundle\Action\Extension\AbstractActionTypeExtension;
+use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class TooltipActionTypeExtension extends AbstractActionTypeExtension
+{
+ public function buildValueView(ActionValueView $view, ActionInterface $column, array $options): void
+ {
+ if (!$options['tooltip']) {
+ return;
+ }
+
+ $title = $view->vars['attr']['title'] ?? null;
+
+ if (empty($title)) {
+ return;
+ }
+
+ $view->vars['attr']['data-bs-toggle'] = 'tooltip';
+ $view->vars['attr']['data-bs-placement'] = 'top';
+ $view->vars['attr']['data-bs-title'] = $title;
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver
+ ->setDefault('tooltip', true)
+ ->setAllowedTypes('tooltip', 'bool')
+ ;
+ }
+
+ public static function getExtendedTypes(): iterable
+ {
+ return [ButtonActionType::class];
+ }
+}
+```
+
+Now, as long as the button action `tooltip` option equals to `true` (by default), and a `title` attribute is set,
+the action will be rendered with Bootstrap tooltip attributes. You can even use the action name instead of the `title` attribute!
+
+## Adding action confirmation
+
+Actions can be configured to require confirmation (by the user) before being executed.
+
+![Action confirmation modal with the Tabler theme](/action_confirmation_modal.png)
+
+To enable action confirmation, set its `confirmation` option to `true`:
+
+```php
+$builder->addRowAction('remove', ButtonActionType::class, [
+ 'confirmation' => true,
+]);
+```
+
+To configure the confirmation modal, pass the array as the `confirmation` option:
+
+```php
+$builder->addRowAction('remove', ButtonActionType::class, [
+ 'confirmation' => [
+ 'translation_domain' => 'KreyuDataTable',
+ 'label_title' => 'Action confirmation',
+ 'label_description' => 'Are you sure you want to execute this action?',
+ 'label_confirm' => 'Confirm',
+ 'label_cancel' => 'Cancel',
+ 'type' => 'danger', // "danger", "warning" or "info"
+ ],
+]);
+```
+
+For reference, see details about the [`confirmation`](#) option.
+
+## Conditionally rendering the action
+
+Action visibility can be configured using its [`visible`](#) option:
+
+```php
+$builder->addRowAction('remove', ButtonActionType::class, [
+ 'visible' => $this->isGranted('ROLE_ADMIN'),
+]);
+```
+
+Another approach would be simply not adding the action at all:
+
+```php
+if ($this->isGranted('ROLE_ADMIN')) {
+ $builder->addRowAction('remove', ButtonActionType::class);
+}
+```
+
+What differentiates those two methods, is that by using the `visible` option, the action is still defined in the data table, but is not rendered in the view.
+It may be useful in some cases, for example, when the actions can be modified outside the data table builder.
+
+## Batch action specifics
+
+### Adding checkbox column
+
+Batch actions require the user to select specific rows. This is handled by the [`CheckboxColumnType`](../../reference/types/column/checkbox.md),
+which simply renders a checkbox with value set to row identifier. To help with that process,
+if at least one batch action is defined, this checkbox column will be added automatically.
+
+This column will be named `__batch`, which can be referenced using the constant:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+
+$column = $builder->getColumn(DataTableBuilderInterface::BATCH_CHECKBOX_COLUMN_NAME);
+```
+
+This behavior can be disabled (or enabled back again) using the builder's method:
+
+```php
+$builder->setAutoAddingBatchCheckboxColumn(false);
+```
+
+### Changing identifier parameter name
+
+By default, the checkbox column type will add the `id` parameter to the batch actions.
+For example, checking rows with ID 1, 2 will result in:
+
+- every batch action's `href` parameter appended with `id[]=1&id[]=2`
+- every batch action's `data-id` parameter set to `[1,2]`
+
+The parameter name can be changed by providing the `identifier_name` option:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Column\Type\CheckboxColumnType;
+
+$builder->addColumn('__batch', CheckboxColumnType::class, [
+ 'identifier_name' => 'product_id',
+]);
+```
+
+Using the above configuration, checking rows with ID 1, 2 will result in:
+
+- every batch action's `href` parameter appended with `product_id[]=1&product_id[]=2`
+- every batch action's `data-product-id` parameter set to `[1,2]`
+
+If the action has no `href` parameter, the query parameters will not be appended.
+The data parameters are not used internally and can be used for custom scripts.
+
+If `FormActionType` is used, the scripts will append hidden inputs with selected values, for example:
+
+```html
+
+
+```
+
+### Changing identifier parameter value
+
+By default, the checkbox column type will try to retrieve the identifier on the `id` property path.
+This can be changed similarly to other column types, by providing the `property_path` option:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Column\Type\CheckboxColumnType;
+
+$builder->addColumn('__batch', CheckboxColumnType::class, [
+ 'property_path' => 'uuid',
+]);
+```
+
+If property accessor is not enough, use the `getter` option:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Column\Type\CheckboxColumnType;
+
+$builder->addColumn('__batch', CheckboxColumnType::class, [
+ 'getter' => fn (Product $product) => $product->getUuid(),
+]);
+```
+
+### Multiple checkbox columns
+
+Using multiple checkbox columns for a single data table is supported.
+For example, using the following configuration:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\Column\Type\CheckboxColumnType;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addColumn('productId', CheckboxColumnType::class, [
+ 'property_path' => 'id',
+ 'identifier_name' => 'product_id',
+ ])
+ ->addColumn('categoryId', CheckboxColumnType::class, [
+ 'property_path' => 'category.id',
+ 'identifier_name' => 'category_id',
+ ])
+ ;
+ }
+}
+```
+
+And having a data set which consists of two rows:
+
+| Product ID | Category ID |
+|------------|-------------|
+| 1 | 3 |
+| 2 | 4 |
+
+Checking the first row's product and second row's category will result in:
+
+- every batch action's `href` parameter appended with `product_id[]=1&category_id[]=4`
+- every batch action's `data-product-id` parameter set to `[1]` and `data-category-id` set to `[4]`
+
+If the action has no `href` parameter, the query parameters will not be appended.
+The data parameters are not used internally and can be used for custom scripts.
+
+If `FormActionType` is used, the scripts will append hidden inputs with selected values, for example:
+
+```html
+
+
+```
\ No newline at end of file
diff --git a/docs/src/docs/components/columns.md b/docs/src/docs/components/columns.md
new file mode 100644
index 00000000..b1b29ec5
--- /dev/null
+++ b/docs/src/docs/components/columns.md
@@ -0,0 +1,276 @@
+# Columns
+
+[[toc]]
+
+## Adding columns
+
+To add a column, use the data table builder's `addColumn()` method:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\Column\Type\NumberColumnType;
+use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType;
+use Kreyu\Bundle\DataTableBundle\Column\Type\DateTimeColumnType;
+
+class UserDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addColumn('id', NumberColumnType::class)
+ ->addColumn('name', TextColumnType::class, [
+ 'label' => 'Full name',
+ ])
+ ->addColumn('createdAt', DateTimeColumnType::class, [
+ 'format' => 'Y-m-d H:i:s',
+ ])
+ ;
+ }
+}
+```
+
+This method accepts _three_ arguments:
+
+- column name;
+- column type — with a fully qualified class name;
+- column options — defined by the column type, used to configure the column;
+
+For reference, see [available column types](../../reference/types/column.md).
+
+## Creating column types
+
+If [built-in column types](../../reference/types/column.md) are not enough, you can create your own.
+In following chapters, we'll be creating a column that renders a phone number stored as an object:
+
+```php
+readonly class PhoneNumber
+{
+ public function __construct(
+ public string $nationalNumber,
+ public string $countryCode,
+ )
+}
+```
+
+Column types are classes that implement [`ColumnTypeInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/ColumnTypeInterface.php), although, it's better to extend from the [`AbstractColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/AbstractColumnType.php):
+
+```php
+use Kreyu\Bundle\DataTableBundle\Column\Type\AbstractColumnType;
+
+class PhoneNumberColumnType extends AbstractColumnType
+{
+}
+```
+
+
+
+Recommended namespace for the column type classes is `App\DataTable\Column\Type\`.
+
+
+
+### Column type inheritance
+
+Because our phone number column fundamentally renders as a text, let's base it off the built-in [`TextColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/TextColumnType.php).
+Provide the fully-qualified class name of the parent type in the `getParent()` method:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Column\Type\AbstractColumnType;
+use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType;
+
+class PhoneNumberColumnType extends AbstractColumnType
+{
+ public function getParent(): ?string
+ {
+ return TextColumnType::class;
+ }
+}
+```
+
+::: tip
+If you take a look at the [`AbstractColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/AbstractColumnType.php),
+you'll see that `getParent()` method returns fully-qualified name of the [`ColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/ColumnType.php) type class.
+This is the type that defines all the basic options, such as `attr`, `label`, etc.
+:::
+
+### Rendering the column type
+
+Because our phone number column is based off the built-in [`TextColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/TextColumnType.php),
+it will be rendered as a text as long as the `PhoneNumber` object can be cast to string. However, in our case, let's store this logic in the template.
+
+First, create a custom theme for the data table, and create a `column_phone_number_value` block:
+
+```twig
+{# templates/data_table/theme.html.twig #}
+
+{% block column_phone_number_value %}
+ +{{ value.countryCode }} {{ value.nationalNumber }}
+{% endblock %}
+```
+
+The block naming follows a set of rules:
+
+- for columns, it always starts with `column` prefix;
+- next comes the block prefix of the column type;
+- last part of the block name represents a part of the column. The column is split into multiple parts when rendering:
+ - `label` - displayed in the column header and in [personalization](../features/personalization.md) column list;
+ - `header` - displayed at the top of the column, allows [sorting](../features/sorting.md) if the column is sortable;
+ - `value` - like shown in example above, it renders the value itself;
+
+If you take a look at the [`AbstractColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/AbstractColumnType.php),
+you'll see that `getBlockPrefix()` returns snake cased short name of the type class, without the `ColumnType` suffix.
+
+In our case, because the type class is named `PhoneNumberColumnType`, the default block prefix equals `phone_number`. Simple as that.
+
+Now, the custom theme should be added to the bundle configuration:
+
+::: code-group
+
+```yaml [YAML]
+kreyu_data_table:
+ defaults:
+ themes:
+ # ...
+ - 'data_table/theme.html.twig'
+```
+
+```php [PHP]
+use Symfony\Config\KreyuDataTableConfig;
+
+return static function (KreyuDataTableConfig $config) {
+ $config->defaults()->themes([
+ // ...
+ 'data_table/theme.html.twig',
+ ]);
+};
+```
+
+:::
+
+
+If the `column_phone_number_value` block wasn't defined in any of the configured themes, the bundle will render block of the parent type.
+In our example, because we set [`TextColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/TextColumnType.php) as a parent, a `column_phone_number_value` block will be rendered.
+
+### Adding configuration options
+
+Column type options allow to configure the behavior of the column types.
+The options are defined in the `configureOptions()` method, using the [OptionsResolver component](https://symfony.com/doc/current/components/options_resolver.html).
+
+Imagine, that you want to determine whether the country code should be rendered. This could be achieved by using a `show_country_code` option:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Column\Type\AbstractColumnType;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class PhoneNumberColumnType extends AbstractColumnType
+{
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver
+ // define available options and their default values
+ ->setDefaults([
+ 'show_country_code' => true,
+ ])
+ // optionally you can restrict type of the options
+ ->setAllowedTypes('country_code', 'bool')
+ ;
+ }
+}
+```
+
+Now you can configure the new option when using the column type:
+
+```php
+class UserDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ // ...
+ ->addColumn('phone', PhoneNumberColumnType::class, [
+ 'show_country_code' => false,
+ ])
+ ;
+ }
+}
+```
+
+### Passing variables to the template
+
+Now, the `show_country_code` option is defined, but is not utilized by the system in any way.
+In our case, we'll pass the options to the view, and use them to render the template itself:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Column\Type\AbstractColumnType;
+use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView;
+use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface;
+
+class PhoneNumberColumnType extends AbstractColumnType
+{
+ public function buildValueView(ColumnValueView $view, ColumnInterface $column, array $options): void
+ {
+ $view->vars['show_country_code'] = $options['show_country_code'];
+ }
+}
+```
+
+Now we can update the template of the type class to use the newly added variable:
+
+```twig
+{# templates/data_table/theme.html.twig #}
+
+{% block column_phone_number_value %}
+ {% if show_country_code %}
+ +{{ value.countryCode }}
+ {% endif %}
+
+ {{ value.nationalNumber }}
+{% endblock %}
+```
+
+## Column type extensions
+
+Column type extensions allows modifying configuration of the existing column types, even the built-in ones.
+Let's assume, that we want to add a `trim` option, which will automatically apply the PHP `trim` method
+on every column type in the system that uses [`TextColumnType`](../../reference/types/column/text.md) as its parent.
+
+Column type extensions are classes that implement [`ColumnTypeExtensionInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Extension/ColumnTypeExtensionInterface.php).
+However, it's better to extend from the [`AbstractColumnTypeExtension`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Extension/AbstractColumnTypeExtension.php):
+
+```php
+use Kreyu\Bundle\DataTableBundle\Column\Extension\AbstractColumnTypeExtension;
+use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class TrimColumnTypeExtension extends AbstractColumnTypeExtension
+{
+ public function buildValueView(ColumnValueView $view, ColumnInterface $column, array $options): void
+ {
+ $value = $view->vars['value'];
+
+ if (!$options['trim'] || !is_string($value)) {
+ return;
+ }
+
+ $view->vars['value'] = trim($value);
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver
+ ->setDefault('trim', true)
+ ->setAllowedTypes('country_code', 'bool')
+ ;
+ }
+
+ public static function getExtendedTypes(): iterable
+ {
+ return [TextColumnType::class];
+ }
+}
+```
+
+Now, automatically, the [`TextColumnType`](../../reference/types/column/text.md) type, as well as every other type that uses it as a parent, have a `trim` option available,
+and its value is trimmed based on this option.
+
+If your extension aims to cover every column type in the system, provide the base [`ColumnType`](../../reference/types/column/column.md) in the `getExtendedTypes()` method.
diff --git a/docs/src/docs/components/exporters.md b/docs/src/docs/components/exporters.md
new file mode 100644
index 00000000..25cb923d
--- /dev/null
+++ b/docs/src/docs/components/exporters.md
@@ -0,0 +1,74 @@
+# Exporters
+
+[[toc]]
+
+## Adding exporters
+
+To add an exporter, use the data table builder's `addExporter()` method:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\Bridge\OpenSpout\Exporter\Type\CsvExporterType;
+use Kreyu\Bundle\DataTableBundle\Bridge\OpenSpout\Exporter\Type\XlsxExporterType;
+
+class UserDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addExporter('csv', CsvExporterType::class)
+ ->addExporter('xlsx', XlsxExporterType::class)
+ ;
+ }
+}
+```
+
+This method accepts _three_ arguments:
+
+- exporter name;
+- exporter type — with a fully qualified class name;
+- exporter options — defined by the exporter type, used to configure the exporter;
+
+For reference, see [available exporter types](../../reference/types/exporter.md).
+
+## Creating exporter types
+
+Exporter types are classes that implement [`ExporterTypeInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/ExporterTypeInterface.php). However, it's better to extend from the [`AbstractExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/AbstractExporterType.php):
+
+```php
+use Kreyu\Bundle\DataTableBundle\Exporter\Type\AbstractExporterType;
+
+class CustomExporterType extends AbstractExporterType
+{
+}
+```
+
+
+
+Recommended namespace for the exporter type classes is `App\DataTable\Exporter\Type\`.
+
+
+
+## Exporter type inheritance
+
+To make a type class use another type as a parent, provide its fully-qualified class name in the `getParent()` method:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Exporter\Type\AbstractExporterType;
+use Kreyu\Bundle\DataTableBundle\Exporter\Type\CallbackExporterType;
+
+class CustomExporterType extends AbstractExporterType
+{
+ public function getParent(): ?string
+ {
+ return CallbackExporterType::class;
+ }
+}
+```
+
+::: tip
+If you take a look at the [`AbstractExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/AbstractExporterType.php),
+you'll see that `getParent()` method returns fully-qualified name of the [`ExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/ExporterType.php) type class.
+This is the type that defines all the basic options, such as `label`, `use_headers`, etc.
+:::
diff --git a/docs/src/docs/components/filters.md b/docs/src/docs/components/filters.md
new file mode 100644
index 00000000..f1cdf9c2
--- /dev/null
+++ b/docs/src/docs/components/filters.md
@@ -0,0 +1,394 @@
+# Filters
+
+[[toc]]
+
+## Adding filters
+
+To add a filter, use the data table builder's `addFilter()` method:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\NumberFilterType;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\DateTimeFilterType;
+
+class UserDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addFilter('id', NumberFilterType::class)
+ ->addFilter('name', TextFilterType::class)
+ ->addFilter('createdAt', DateTimeFilterType::class)
+ ;
+ }
+}
+```
+
+This method accepts _three_ arguments:
+
+- filter name;
+- filter type — with a fully qualified class name;
+- filter options — defined by the filter type, used to configure the filter;
+
+For reference, see [available filter types](../../reference/types/filter.md).
+
+## Creating filter types
+
+This bundle and integrations, such as [Doctrine ORM integration bundle](../integrations/doctrine-orm/installation.md), come with plenty of the [filter types](../../reference/types/filter.md).
+However, those may not cover complex cases. Luckily, creating custom filter types are easy.
+
+Filter types are classes that implement [`FilterTypeInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/FilterTypeInterface.php). However, it's better to extend from the [`AbstractFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/AbstractFilterType.php):
+
+```php
+use Kreyu\Bundle\DataTableBundle\Filter\Type\AbstractFilterType;
+
+class PhoneNumberFilterType extends AbstractFilterType
+{
+}
+```
+
+
+
+Recommended namespace for the filter type classes is `App\DataTable\Filter\Type\`.
+
+
+
+### Filter type inheritance
+
+If you take a look at the [`AbstractFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/AbstractFilterType.php), you'll see that `getParent()` method returns fully-qualified name of the `FilterType` type class.
+This is the type that defines all the required options, such as `label`, `form_type`, `form_options`, etc.
+
+::: danger This is not recommended: do _not_ use PHP inheritance!
+```php
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType;
+
+class PhoneNumberFilterType extends TextFilterType
+{
+}
+```
+:::
+
+::: tip This is recommended: provide parent using the `getParent()` method
+```php
+use Kreyu\Bundle\DataTableBundle\Filter\Type\AbstractFilterType;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType;
+
+class PhoneNumberFilterType extends AbstractFilterType
+{
+ public function getParent(): ?string
+ {
+ return TextFilterType::class;
+ }
+}
+```
+:::
+
+Both methods _will work_, but using PHP inheritance may result in unexpected behavior when using the [filter type extensions](#filter-type-extensions).
+
+### Form type and options
+
+To define form type and its options for the filter, use `form_type` and `form_options` options:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Filter\Type\AbstractFilterType;
+use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class ColorFilterType extends AbstractFilterType
+{
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'form_type' => ChoiceType::class,
+ 'form_options' => [
+ 'choices' => [
+ '#F44336' => 'Red',
+ '#4CAF50' => 'Green',
+ '#2196F3' => 'Blue',
+ ],
+ ],
+ ]);
+ }
+}
+```
+
+### Creating filter handler
+
+Filter type classes is used to define the filter, not the actual logic executed when the filter is used.
+This logic should be delegated to a filter handler instead. Filter handlers are classes that implement [`FilterHandlerInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/FilterHandlerInterface.php):
+
+```php
+use Kreyu\Bundle\DataTableBundle\Filter\FilterHandlerInterface;
+use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
+
+class CustomFilterHandler implements FilterHandlerInterface
+{
+ public function handle(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter): void
+ {
+ // ...
+ }
+}
+```
+
+
+For example, take a look at the [`DoctrineOrmFilterHandler`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/DoctrineOrmFilterHandler.php),
+which is used by all Doctrine ORM integration filter types.
+
+
+
+The filter handler can be applied to a custom filter type by using the filter builder's `setHandler()` method:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Filter\Type\AbstractFilterType;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterBuilderInterface;
+
+class CustomFilterType extends AbstractFilterType
+{
+ public function buildFilter(FilterBuilderInterface $builder, array $options): void
+ {
+ $builder->setHandler(new CustomFilterHandler());
+ }
+}
+```
+
+If separate class seems like an overkill, you can implement the handler interface on the type class instead:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Filter\Type\AbstractFilterType;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterHandlerInterface;
+use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
+
+class CustomFilterType extends AbstractFilterType implements FilterHandlerInterface
+{
+ public function buildFilter(FilterBuilderInterface $builder, array $options): void
+ {
+ $builder->setHandler($this);
+ }
+
+ public function handle(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter): void
+ {
+ // ...
+ }
+}
+```
+
+## Filter type extensions
+
+Filter type extensions allows modifying configuration of the existing filter types, even the built-in ones.
+Let's assume, that we want to [change default operator](#changing-default-operator) of [`TextFilterType`](#)
+to `Operator::Equals`, so it is not necessary to pass `default_operator` option for each filter using this type.
+
+Filter type extensions are classes that implement [`FilterTypeExtensionInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Extension/FilterTypeExtensionInterface.php).
+However, it's better to extend from the [`AbstractFilterTypeExtension`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Extension/AbstractColumnTypeExtension.php):
+
+```php
+use Kreyu\Bundle\DataTableBundle\Filter\Extension\AbstractFilterTypeExtension;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType;
+use Kreyu\Bundle\DataTableBundle\Filter\Operator;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class DefaultOperatorTextFilterTypeExtension extends AbstractFilterTypeExtension
+{
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefault('default_operator', Operator::Equals);
+ }
+
+ public static function getExtendedTypes(): iterable
+ {
+ return [TextFilterType::class];
+ }
+}
+```
+
+If your extension aims to cover every filter type in the system, provide the base [`FilterType`](#) in the `getExtendedTypes()` method.
+
+## Formatting active filter value
+
+When the filter is active, its value is rendered to the user as a "pill", which removes the filter upon clicking it.
+By default, the filter value requires to be stringable. However, there are some cases, where value cannot be stringable.
+
+Let's assume, that the application contains a `Product` entity, which contains a `Category`, which is **not** stringable:
+
+```php
+readonly class Product
+{
+ public function __construct(
+ public Category $category,
+ )
+}
+
+readonly class Category
+{
+ public function __construct(
+ public string $name,
+ )
+}
+```
+
+In the product data table, we want to filter products by their category.
+Using [EntityFilterType](#) will allow selecting a category from a list of existing categories.
+Unfortunately, when the filter is applied, a `Cannot convert value of type Category to string` exception will occur.
+
+In that case, you can use the `active_filter_formatter` option, to determine what should be rendered based on the filter data:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\EntityFilterType;
+use Kreyu\Bundle\DataTableBundle\Filter\Operator;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addFilter('category', EntityFilterType::class, [
+ 'form_options' => [
+ 'class' => Category::class,
+ 'choice_label' => 'name',
+ ],
+ 'active_filter_formatter' => function (FilterData $data) {
+ $value = $data->getValue();
+
+ if ($value instanceof Category) {
+ return $value->getName();
+ }
+
+ return $value;
+ },
+ ])
+ ;
+ }
+}
+```
+
+::: tip This is only a simple example of using the `active_filter_formatter` option.
+The [`EntityFilterType`](#) has a `choice_label` option, which can be used to provide property path to the value to render:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\EntityFilterType;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addFilter('category', EntityFilterType::class, [
+ 'form_options' => [
+ 'class' => Category::class,
+ 'choice_label' => 'name',
+ ],
+ 'choice_label' => 'name', // same as the form choice_label option
+ ])
+ ;
+ }
+}
+```
+:::
+
+## Changing default operator
+
+Let's assume, that the application contains a `Book` entity with ISBN:
+
+```php
+readonly class Book
+{
+ public function __construct(
+ public string $isbn,
+ )
+}
+```
+
+If we use a [TextFilterType](#) on the `isbn` column, the filter will perform partial matching (`LIKE %value%`),
+because the filter type has `default_operator` option set to `Operator::Contains`.
+In this case, we want to perform exact matching, therefore, we have to change this option value to `Operator::Equals`:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType;
+use Kreyu\Bundle\DataTableBundle\Filter\Operator;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addFilter('isbn', TextFilterType::class, [
+ 'default_operator' => Operator::Equals,
+ ])
+ ;
+ }
+}
+```
+
+
+
+Each filter supports different set of operators.
+
+
+
+
+
+To change default operator filter type without having to explicitly provide the `default_operator`,
+consider creating a [filter type extension](#filter-type-extensions).
+
+
+
+## Displaying operator selector
+
+The operator can be selected by the user, when operator selector is visible.
+By default, operator selector is **not** visible. To change that, use `operator_visible` option:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType;
+use Kreyu\Bundle\DataTableBundle\Filter\Operator;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addFilter('isbn', TextFilterType::class, [
+ 'operator_visible' => true,
+ ])
+ ;
+ }
+}
+```
+
+## Operator form type and options
+
+You can customize form type and options of the operator form field, using `operator_form_type` and `operator_form_options`:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Filter\Type\AbstractFilterType;
+use Kreyu\Bundle\DataTableBundle\Filter\Form\Type\OperatorType;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class ProductFilterType extends AbstractFilterType
+{
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ // Note: this is default operator type
+ 'operator_form_type' => OperatorType::class,
+ 'operator_form_options' => [
+ 'required' => true,
+ ],
+ ]);
+ }
+}
+```
diff --git a/docs/src/docs/contributing.md b/docs/src/docs/contributing.md
new file mode 100644
index 00000000..d1eab678
--- /dev/null
+++ b/docs/src/docs/contributing.md
@@ -0,0 +1,13 @@
+# Contributing
+
+## Documentation
+
+The documentation is powered by the [VitePress](https://vitepress.dev/).
+
+To locally preview the documentation, first, install the [VitePress](https://vitepress.dev/) locally.
+The installation instructions are available in the ["Getting Started" documentation section](https://vitepress.dev/guide/getting-started).
+Then, to build the documentation locally (and rebuild when change is detected), run the following command:
+
+```shell
+npm install && npm run docs:dev
+```
diff --git a/docs/src/docs/features/asynchronicity.md b/docs/src/docs/features/asynchronicity.md
new file mode 100644
index 00000000..cd417980
--- /dev/null
+++ b/docs/src/docs/features/asynchronicity.md
@@ -0,0 +1,33 @@
+# Asynchronicity
+
+[Symfony UX Turbo](https://symfony.com/bundles/ux-turbo/current/index.html) is a Symfony bundle integrating the [Hotwire Turbo](https://turbo.hotwired.dev/) library in Symfony applications.
+It allows having the same user experience as with [Single Page Apps](https://en.wikipedia.org/wiki/Single-page_application) but without having to write a single line of JavaScript!
+
+This bundle provides integration that works out-of-the-box.
+
+## The magic part
+
+Make sure your application uses the [Symfony UX Turbo](https://symfony.com/bundles/ux-turbo/current/index.html).
+You don't have to configure anything extra, your data tables automatically work asynchronously!
+The magic comes from the [base template](https://github.com/Kreyu/data-table-bundle/blob/main/src/Resources/views/themes/base.html.twig),
+which wraps the whole table in the `` tag:
+
+```twig
+{# @KreyuDataTable/themes/base.html.twig #}
+{% block kreyu_data_table %}
+
+ {# ... #}
+
+{% endblock %}
+```
+
+This ensures every data table is wrapped in its own frame, making them work asynchronously.
+
+
+
+This integration also works on other built-in templates, because they all extend the base one.
+If you're making a data table theme from scratch, make sure the table is wrapped in the Turbo frame, as shown above.
+
+
+
+For more information, see [official documentation about the Turbo frames](https://symfony.com/bundles/ux-turbo/current/index.html#decomposing-complex-pages-with-turbo-frames).
diff --git a/docs/src/docs/features/exporting.md b/docs/src/docs/features/exporting.md
new file mode 100644
index 00000000..c69378e6
--- /dev/null
+++ b/docs/src/docs/features/exporting.md
@@ -0,0 +1,301 @@
+# Exporting
+
+The data tables can be _exported_, with use of the [exporters](#).
+
+::: details Screenshots
+![Export modal with the Tabler theme](/export_modal.png)
+:::
+
+[[toc]]
+
+## Toggling the feature
+
+By default, the exporting feature is **enabled** for every data table.
+This can be configured with the `exporting_enabled` option:
+
+::: code-group
+```yaml [Globally (YAML)]
+kreyu_data_table:
+ defaults:
+ exporting:
+ enabled: true
+```
+
+```php [Globally (PHP)]
+use Symfony\Config\KreyuDataTableConfig;
+
+return static function (KreyuDataTableConfig $config) {
+ $defaults = $config->defaults();
+ $defaults->exporting()->enabled(true);
+};
+```
+
+```php [For data table type]
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'exporting_enabled' => true,
+ ]);
+ }
+}
+```
+
+```php [For specific data table]
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function index()
+ {
+ $dataTable = $this->createDataTable(
+ type: ProductDataTableType::class,
+ query: $query,
+ options: [
+ 'exporting_enabled' => true,
+ ],
+ );
+ }
+}
+```
+:::
+
+::: tip Enabling the feature does not mean that any column will be exportable by itself.
+By default, columns **are not** exportable.
+:::
+
+## Making the columns exportable
+
+To make any column exportable, use its `export` option:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Column\Type\NumberColumnType;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addColumn('id', NumberColumnType::class, [
+ 'export' => true,
+ ])
+ ;
+ }
+}
+```
+
+The column can be configured separately for the export by providing the array in the `export` option.
+For example, to change the label of the column in the export:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addColumn('category', TextColumnType::class, [
+ 'export' => [
+ 'label' => 'Category Name',
+ ],
+ ])
+ ;
+ }
+}
+```
+
+## Default export configuration
+
+The default export data, such as filename, exporter, strategy and a flag whether the personalization should be included,
+can be configured using the data table builder's `setDefaultExportData()` method:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Exporter\ExportData;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->setDefaultExportData(ExportData::fromArray([
+ 'filename' => sprintf('products_%s', date('Y-m-d')),
+ 'exporter' => 'xlsx',
+ 'strategy' => ExportStrategy::IncludeAll,
+ 'include_personalization' => true,
+ ]))
+ ;
+ }
+}
+```
+
+## Handling the export form
+
+In the controller, use the `isExporting()` method to make sure the request should be handled as an export:
+
+```php
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\Request;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function index(Request $request)
+ {
+ $dataTable = $this->createDataTable(ProductDataTableType::class);
+ $dataTable->handleRequest($request);
+
+ if ($dataTable->isExporting()) {
+ return $this->file($dataTable->export());
+ }
+ }
+}
+```
+
+## Exporting without user input
+
+To export the data table manually, without user input, use the `export()` method directly:
+
+```php
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function index()
+ {
+ $dataTable = $this->createDataTable(ProductDataTableType::class);
+
+ // An instance of ExportFile, which extends the HttpFoundation File object
+ $file = $dataTable->export();
+
+ // For example, save it manually:
+ $file->move(__DIR__);
+
+ // Or return a BinaryFileResponse to download it in browser:
+ return $this->file($file);
+ }
+}
+```
+
+The export data (configuration, e.g. a filename) can be included by passing it directly to the `export()` method:
+
+```php
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function index()
+ {
+ $dataTable = $this->createDataTable(ProductDataTableType::class);
+
+ $exportData = ExportData::fromDataTable($dataTable);
+ $exportData->filename = sprintf('products_%s', date('Y-m-d'));
+ $exportData->includePersonalization = false;
+
+ $file = $dataTable->export($exportData);
+
+ // ...
+ }
+}
+```
+
+## Optimization with Doctrine ORM
+
+The exporting process including all pages of the large datasets can take a very long time.
+To optimize this process, when using Doctrine ORM, change the hydration mode to array during the export:
+
+```php
+use Doctrine\ORM\AbstractQuery;
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Event\DataTableEvent;
+use Kreyu\Bundle\DataTableBundle\Event\DataTableEvents;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder->addEventListener(DataTableEvents::PRE_EXPORT, function (DataTableEvent $event) {
+ $event->getDataTable()->getQuery()->setHydrationMode(AbstractQuery::HYDRATE_ARRAY);
+ });
+ }
+}
+```
+
+This will prevent the Doctrine ORM from hydrating the entities, which is not needed for the export.
+Unfortunately, this means each exportable column property path has to be changed to array (wrapped in square brackets):
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Column\Type\NumberColumnType;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addColumn('id', NumberColumnType::class, [
+ 'export' => [
+ 'property_path' => '[id]',
+ ],
+ ])
+ ;
+ }
+}
+
+```
+
+## Events
+
+The following events are dispatched when `export()` method of the [`DataTableInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/DataTableInterface.php) is called:
+
+::: info PRE_EXPORT
+Dispatched before the exporter is called.
+Can be used to modify the exporting data (configuration), e.g. to force an export strategy or change the filename.
+
+**See**: [`DataTableEvents::PRE_EXPORT`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php)
+:::
+
+The dispatched events are instance of the [`DataTablePersonalizationEvent`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTablePersonalizationEvent.php):
+
+```php
+use Kreyu\Bundle\DataTableBundle\Event\DataTableExportEvent;
+
+class DataTableExportListener
+{
+ public function __invoke(DataTableExportEvent $event): void
+ {
+ $dataTable = $event->getDataTable();
+ $exportData = $event->getExportData();
+
+ // for example, modify the export data (configuration), then save it in the event
+ $event->setExportData($exportData);
+ }
+}
+```
\ No newline at end of file
diff --git a/docs/src/docs/features/extensibility.md b/docs/src/docs/features/extensibility.md
new file mode 100644
index 00000000..4bd655d5
--- /dev/null
+++ b/docs/src/docs/features/extensibility.md
@@ -0,0 +1,270 @@
+# Extensibility
+
+There are multiple concepts that can be modified for a specific case.
+
+[[toc]]
+
+## Request handlers
+
+The data tables by default have no clue about the requests.
+To solve this problem, a request can be handled by the data table using the `handleRequest()` method.
+This means an underlying request handler will be called, extracting the required data from the request,
+and calling methods such as `sort()` or `paginate()` on the data table.
+
+### Built-in request handlers
+
+This bundle comes with [HttpFoundationRequestHandler](https://github.com/Kreyu/data-table-bundle/blob/main/src/Request/HttpFoundationRequestHandler.php),
+which supports the [request object](https://github.com/symfony/http-foundation/blob/6.4/Request.php) common for the Symfony applications:
+
+```php
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\Request;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function index(Request $request)
+ {
+ $dataTable = $this->createDataTable(ProductDataTableType::class);
+ $dataTable->handleRequest($request);
+ }
+}
+```
+
+### Creating request handlers
+
+To create a request handler, create a class that implements [RequestHandlerInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Request/RequestHandlerInterface.php):
+
+```php
+use Kreyu\Bundle\DataTableBundle\Request\RequestHandlerInterface;
+use Kreyu\Bundle\DataTableBundle\Sorting\SortingData;
+use Kreyu\Bundle\DataTableBundle\Sorting\SortingField;
+
+class CustomRequestHandler implements RequestHandlerInterface
+{
+ public function handle(DataTableInterface $dataTable, mixed $request = null): void
+ {
+ // Call desired methods with arguments based on the data from $request
+ $dataTable->paginate(...);
+ $dataTable->sort(...);
+ $dataTable->personalize(...);
+ $dataTable->filter(...);
+ $dataTable->export(...);
+ }
+}
+```
+
+
+
+The recommended namespace for the request handlers is `App\DataTable\Request`.
+
+
+
+You can apply this request handler globally using the configuration file, or use `request_handler` option:
+
+::: code-group
+```yaml [Globally (YAML)]
+kreyu_data_table:
+ defaults:
+ # this should be a service id - which is class by default
+ request_handler: 'App\DataTable\Request\CustomRequestHandler'
+```
+
+```php [Globally (PHP)]
+use Symfony\Config\KreyuDataTableConfig;
+
+return static function (KreyuDataTableConfig $config) {
+ $defaults = $config->defaults();
+ // this should be a service id - which is class by default
+ $defaults->requestHandler('App\DataTable\Request\CustomRequestHandler');
+};
+```
+
+```php [For data table type]
+use App\DataTable\Request\CustomRequestHandler;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function __construct(
+ private CustomRequestHandler $requestHandler,
+ ) {
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'request_handler' => $this->requestHandler,
+ ]);
+ }
+}
+```
+
+```php [For specific data table]
+use App\DataTable\Request\CustomRequestHandler;
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function __construct(
+ private CustomRequestHandler $requestHandler,
+ ) {
+ }
+
+ public function index()
+ {
+ $dataTable = $this->createDataTable(
+ type: ProductDataTableType::class,
+ query: $query,
+ options: [
+ 'request_handler' => $this->requestHandler,
+ ],
+ );
+ }
+}
+```
+:::
+
+## Proxy queries
+
+This bundle is data source agnostic, meaning it is not tied to any specific ORM, such as Doctrine ORM.
+This is accomplished thanks to **proxy queries**, which work as an adapter for the specific data source.
+
+### Creating custom proxy query
+
+To create a custom proxy query, create a class that implements [ProxyQueryInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Query/ProxyQueryInterface.php):
+
+```php
+use Kreyu\Bundle\DataTableBundle\Pagination\PaginationData;
+use Kreyu\Bundle\DataTableBundle\Sorting\SortingData;
+use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
+use Kreyu\Bundle\DataTableBundle\Query\ResultSetInterface;
+
+class ArrayProxyQuery implements ProxyQueryInterface
+{
+ public function __construct(
+ private array $data,
+ ) {
+ }
+
+ public function sort(SortingData $sortingData): void
+ {
+ }
+
+ public function paginate(PaginationData $paginationData): void
+ {
+ }
+
+ public function getResult(): ResultSetInterface
+ {
+ }
+}
+```
+
+
+
+The recommended namespace for the proxy queries is `App\DataTable\Query`.
+
+
+
+Now you can use the custom proxy query when creating the data tables:
+
+```php
+use App\DataTable\Type\ProductDataTableType;
+use App\DataTable\Query\ArrayProxyQuery;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function index()
+ {
+ // Note: the products are an instance of ArrayProxyQuery
+ $products = new ArrayProxyQuery([
+ new Product(name: 'Product #1'),
+ new Product(name: 'Product #2'),
+ new Product(name: 'Product #3'),
+ ]);
+
+ $dataTable = $this->createDataTable(ProductDataTableType::class, $products);
+ }
+}
+```
+
+### Creating proxy query factory
+
+Each proxy query should have a factory, so the bundle can handle passing the raw data like so:
+
+```php
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function index()
+ {
+ // Note: products are just a simple array, ArrayProxyQuery is not required
+ $products = [
+ new Product(name: 'Product #1'),
+ new Product(name: 'Product #2'),
+ new Product(name: 'Product #3'),
+ ];
+
+ $dataTable = $this->createDataTable(ProductDataTableType::class, $products);
+ }
+}
+```
+
+Without dedicated proxy query factory to handle array data, the bundle will throw an exception:
+
+> Unable to create ProxyQuery for given data
+
+In the background, the [ChainProxyQueryFactory](https://github.com/Kreyu/data-table-bundle/blob/main/src/Query/ChainProxyQueryFactory.php)
+iterates through registered proxy query factories, and returns the first successfully created proxy query.
+The error occurs because there is no factory to create the custom type.
+
+To create a proxy query factory, create a class that implements the [ProxyQueryFactoryInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Query/ProxyQueryFactoryInterface.php):
+
+```php
+use App\DataTable\Query\ArrayProxyQuery;
+use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryFactoryInterface;
+use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
+
+class ArrayProxyQueryFactory implements ProxyQueryFactoryInterface
+{
+ public function create(mixed $data): ProxyQueryInterface
+ {
+ if (!is_array($data)) {
+ throw new UnexpectedTypeException($data, ArrayProxyQuery::class);
+ }
+
+ return new ArrayProxyQuery($data);
+ }
+}
+```
+
+
+
+The recommended namespace for the proxy query factories is `App\DataTable\Query`.
+
+
+
+If the custom proxy query does not support a specific data class, the factory **have** to throw an [UnexpectedTypeException](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exception/UnexpectedTypeException.php),
+so the chain proxy query factory will know to skip that factory and check other ones.
+
+Proxy query factories must be registered as services and tagged with the `kreyu_data_table.proxy_query.factory` tag.
+If you're using the [default services.yaml configuration](https://symfony.com/doc/current/service_container.html#service-container-services-load-example),
+this is already done for you, thanks to [autoconfiguration](https://symfony.com/doc/current/service_container.html#services-autoconfigure).
diff --git a/docs/src/docs/features/filtering.md b/docs/src/docs/features/filtering.md
new file mode 100644
index 00000000..16a865dd
--- /dev/null
+++ b/docs/src/docs/features/filtering.md
@@ -0,0 +1,225 @@
+# Filtering
+
+The data tables can be _filtered_, with use of the [filters](#).
+
+[[toc]]
+
+## Toggling the feature
+
+By default, the filtration feature is **enabled** for every data table.
+This can be configured with the `filtration_enabled` option:
+
+::: code-group
+```yaml [Globally (YAML)]
+kreyu_data_table:
+ defaults:
+ filtration:
+ enabled: true
+```
+
+```php [Globally (PHP)]
+use Symfony\Config\KreyuDataTableConfig;
+
+return static function (KreyuDataTableConfig $config) {
+ $defaults = $config->defaults();
+ $defaults->filtration()->enabled(true);
+};
+```
+
+```php [For data table type]
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'filtration_enabled' => true,
+ ]);
+ }
+}
+```
+
+```php [For specific data table]
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function index()
+ {
+ $dataTable = $this->createDataTable(
+ type: ProductDataTableType::class,
+ query: $query,
+ options: [
+ 'filtration_enabled' => true,
+ ],
+ );
+ }
+}
+```
+:::
+
+## Saving applied filters
+
+By default, the filtration feature [persistence](persistence.md) is **disabled** for every data table.
+
+You can configure the [persistence](persistence.md) globally using the package configuration file, or its related options:
+
+::: code-group
+```yaml [Globally (YAML)]
+kreyu_data_table:
+ defaults:
+ filtration:
+ persistence_enabled: true
+ # if persistence is enabled and symfony/cache is installed, null otherwise
+ persistence_adapter: kreyu_data_table.filtration.persistence.adapter.cache
+ # if persistence is enabled and symfony/security-bundle is installed, null otherwise
+ persistence_subject_provider: kreyu_data_table.persistence.subject_provider.token_storage
+```
+
+```php [Globally (PHP)]
+use Symfony\Config\KreyuDataTableConfig;
+
+return static function (KreyuDataTableConfig $config) {
+ $defaults = $config->defaults();
+ $defaults->filtration()
+ ->persistenceEnabled(true)
+ // if persistence is enabled and symfony/cache is installed, null otherwise
+ ->persistenceAdapter('kreyu_data_table.filtration.persistence.adapter.cache')
+ // if persistence is enabled and symfony/security-bundle is installed, null otherwise
+ ->persistenceSubjectProvider('kreyu_data_table.persistence.subject_provider.token_storage')
+ ;
+};
+```
+
+```php [For data table type]
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface;
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function __construct(
+ #[Autowire(service: 'kreyu_data_table.filtration.persistence.adapter.cache')]
+ private PersistenceAdapterInterface $persistenceAdapter,
+ #[Autowire(service: 'kreyu_data_table.persistence.subject_provider.token_storage')]
+ private PersistenceSubjectProviderInterface $persistenceSubjectProvider,
+ ) {
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'filtration_persistence_enabled' => true,
+ 'filtration_persistence_adapter' => $this->persistenceAdapter,
+ 'filtration_persistence_subject_provider' => $this->persistenceSubjectProvider,
+ ]);
+ }
+}
+```
+
+```php [For specific data table]
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface;
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function __construct(
+ #[Autowire(service: 'kreyu_data_table.filtration.persistence.adapter.cache')]
+ private PersistenceAdapterInterface $persistenceAdapter,
+ #[Autowire(service: 'kreyu_data_table.persistence.subject_provider.token_storage')]
+ private PersistenceSubjectProviderInterface $persistenceSubjectProvider,
+ ) {
+ }
+
+ public function index()
+ {
+ $dataTable = $this->createDataTable(
+ type: ProductDataTableType::class,
+ query: $query,
+ options: [
+ 'filtration_persistence_enabled' => true,
+ 'filtration_persistence_adapter' => $this->persistenceAdapter,
+ 'filtration_persistence_subject_provider' => $this->persistenceSubjectProvider,
+ ],
+ );
+ }
+}
+```
+:::
+
+## Default filtration
+
+The default filtration data can be overridden using the data table builder's `setDefaultFiltrationData()` method:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\Filter\FiltrationData;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
+use Kreyu\Bundle\DataTableBundle\Filter\Operator;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder->setDefaultFiltrationData(new FiltrationData([
+ 'name' => new FilterData(value: 'John', operator: Operator::Contains),
+ ]));
+
+ // or by creating the filtration data from an array:
+ $builder->setDefaultFiltrationData(FiltrationData::fromArray([
+ 'name' => ['value' => 'John', 'operator' => 'contains'],
+ ]));
+ }
+}
+```
+
+## Events
+
+The following events are dispatched when `filter()` method of the [`DataTableInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/DataTableInterface.php) is called:
+
+::: info PRE_FILTER
+Dispatched before the filtration data is applied to the query.
+Can be used to modify the filtration data, e.g. to force application of some filters.
+
+**See**: [`DataTableEvents::PRE_FILTER`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php)
+:::
+
+::: info POST_FILTER
+Dispatched after the filtration data is applied to the query and saved if the filtration persistence is enabled;
+Can be used to execute additional logic after the filters are applied.
+
+**See**: [`DataTableEvents::POST_FILTER`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php)
+:::
+
+The dispatched events are instance of the [`DataTableFiltrationEvent`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableFiltrationEvent.php):
+
+```php
+use Kreyu\Bundle\DataTableBundle\Event\DataTableFiltrationEvent;
+
+class DataTableFiltrationListener
+{
+ public function __invoke(DataTableFiltrationEvent $event): void
+ {
+ $dataTable = $event->getDataTable();
+ $filtrationData = $event->getFiltrationData();
+
+ // for example, modify the filtration data, then save it in the event
+ $event->setFiltrationData($filtrationData);
+ }
+}
+```
\ No newline at end of file
diff --git a/docs/src/docs/features/pagination.md b/docs/src/docs/features/pagination.md
new file mode 100644
index 00000000..07a6f1cf
--- /dev/null
+++ b/docs/src/docs/features/pagination.md
@@ -0,0 +1,225 @@
+# Pagination
+
+The data tables can be _paginated_, which is crucial when working with large data sources.
+
+[[toc]]
+
+## Toggling the feature
+
+By default, the pagination feature is **enabled** for every data table.
+This can be configured thanks to the `pagination_enabled` option:
+
+::: code-group
+```yaml [Globally (YAML)]
+kreyu_data_table:
+ defaults:
+ pagination:
+ enabled: true
+```
+
+```php [Globally (PHP)]
+use Symfony\Config\KreyuDataTableConfig;
+
+return static function (KreyuDataTableConfig $config) {
+ $defaults = $config->defaults();
+ $defaults->pagination()->enabled(true);
+};
+```
+
+```php [For data table type]
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'pagination_enabled' => true,
+ ]);
+ }
+}
+```
+
+```php [For specific data table]
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function index()
+ {
+ $dataTable = $this->createDataTable(
+ type: ProductDataTableType::class,
+ query: $query,
+ options: [
+ 'pagination_enabled' => true,
+ ],
+ );
+ }
+}
+```
+:::
+
+::: tip If you don't see the pagination controls, make sure your data table has enough records!
+By default, every page contains 25 records.
+Built-in themes display pagination controls only when the data table contains more than one page.
+Also, remember that you can [change the default pagination data](#default-pagination), reducing the per-page limit.
+:::
+
+## Saving applied pagination
+
+By default, the pagination feature [persistence](persistence.md) is **disabled** for every data table.
+
+You can configure the [persistence](persistence.md) globally using the package configuration file, or its related options:
+
+::: code-group
+```yaml [Globally (YAML)]
+kreyu_data_table:
+ defaults:
+ pagination:
+ persistence_enabled: true
+ # if persistence is enabled and symfony/cache is installed, null otherwise
+ persistence_adapter: kreyu_data_table.sorting.persistence.adapter.cache
+ # if persistence is enabled and symfony/security-bundle is installed, null otherwise
+ persistence_subject_provider: kreyu_data_table.persistence.subject_provider.token_storage
+```
+
+```php [Globally (PHP)]
+use Symfony\Config\KreyuDataTableConfig;
+
+return static function (KreyuDataTableConfig $config) {
+ $defaults = $config->defaults();
+ $defaults->pagination()
+ ->persistenceEnabled(true)
+ // if persistence is enabled and symfony/cache is installed, null otherwise
+ ->persistenceAdapter('kreyu_data_table.sorting.persistence.adapter.cache')
+ // if persistence is enabled and symfony/security-bundle is installed, null otherwise
+ ->persistenceSubjectProvider('kreyu_data_table.persistence.subject_provider.token_storage')
+ ;
+};
+```
+
+```php [For data table type]
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface;
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function __construct(
+ private PersistenceAdapterInterface $persistenceAdapter,
+ private PersistenceSubjectProviderInterface $persistenceSubjectProvider,
+ ) {
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'pagination_persistence_enabled' => true,
+ 'pagination_persistence_adapter' => $this->persistenceAdapter,
+ 'pagination_persistence_subject_provider' => $this->persistenceSubjectProvider,
+ ]);
+ }
+}
+```
+
+```php [For specific data table]
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface;
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function __construct(
+ private PersistenceAdapterInterface $persistenceAdapter,
+ private PersistenceSubjectProviderInterface $persistenceSubjectProvider,
+ ) {
+ }
+
+ public function index()
+ {
+ $dataTable = $this->createDataTable(
+ type: ProductDataTableType::class,
+ query: $query,
+ options: [
+ 'pagination_persistence_enabled' => true,
+ 'pagination_persistence_adapter' => $this->persistenceAdapter,
+ 'pagination_persistence_subject_provider' => $this->persistenceSubjectProvider,
+ ],
+ );
+ }
+}
+```
+:::
+
+## Default pagination
+
+The default pagination data can be overridden using the data table builder's `setDefaultPaginationData()` method:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\Pagination\PaginationData;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder->setDefaultPaginationData(new PaginationData(
+ page: 1,
+ perPage: 25,
+ ));
+
+ // or by creating the pagination data from an array:
+ $builder->setDefaultPaginationData(PaginationData::fromArray([
+ 'page' => 1,
+ 'perPage' => 25,
+ ]));
+ }
+}
+```
+
+## Events
+
+The following events are dispatched when `paginate()` method of the [`DataTableInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/DataTableInterface.php) is called:
+
+::: info PRE_PAGINATE
+Dispatched before the pagination data is applied to the query.
+Can be used to modify the pagination data, e.g. to force specific page or a per-page limit.
+
+**See**: [`DataTableEvents::PRE_PAGINATE`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php)
+:::
+
+::: info POST_PAGINATE
+Dispatched after the pagination data is applied to the query and saved if the pagination persistence is enabled.
+Can be used to execute additional logic after the pagination is applied.
+
+**See**: [`DataTableEvents::POST_PAGINATE`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php)
+:::
+
+The dispatched events are instance of the [`DataTablePaginationEvent`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTablePaginationEvent.php):
+
+```php
+use Kreyu\Bundle\DataTableBundle\Event\DataTablePaginationEvent;
+
+class DataTablePaginationListener
+{
+ public function __invoke(DataTablePaginationEvent $event): void
+ {
+ $dataTable = $event->getDataTable();
+ $paginationData = $event->getPaginationData();
+
+ // for example, modify the pagination data, then save it in the event
+ $event->setPaginationData($paginationData);
+ }
+}
+```
diff --git a/docs/src/docs/features/persistence.md b/docs/src/docs/features/persistence.md
new file mode 100644
index 00000000..73645597
--- /dev/null
+++ b/docs/src/docs/features/persistence.md
@@ -0,0 +1,405 @@
+# Persistence
+
+This bundle provides a persistence feature, which is used to save data between requests.
+For example, it can be used to persist applied filters or pagination, per user.
+
+[[toc]]
+
+## Toggling the feature
+
+Persistence can be toggled per feature with its own configuration:
+
+- [Saving applied pagination](pagination.md#saving-applied-pagination)
+- [Saving applied sorting](sorting.md#configuring-the-feature-persistence)
+- [Saving applied filters](filtering.md#configuring-the-feature-persistence)
+- [Saving applied personalization](personalization.md#saving-applied-personalization)
+
+## Persistence adapters
+
+Adapters are classes that allow writing (to) and reading (from) the persistent data source.
+
+### Built-in cache adapter
+
+The bundle has a built-in cache adapter, which uses the [Symfony Cache component](https://symfony.com/doc/current/components/cache.html).
+
+It is registered as an [abstract service](https://symfony.com/doc/current/service_container/parent_services.html) in the service container:
+
+```bash
+$ bin/console debug:container kreyu_data_table.persistence.adapter.cache
+```
+
+The adapters are then created based on the abstract definition:
+
+```bash
+$ bin/console debug:container kreyu_data_table.pagination.persistence.adapter.cache
+$ bin/console debug:container kreyu_data_table.sorting.persistence.adapter.cache
+$ bin/console debug:container kreyu_data_table.filtration.persistence.adapter.cache
+$ bin/console debug:container kreyu_data_table.personalization.persistence.adapter.cache
+```
+
+The bundle adds a `kreyu_data_table.persistence.cache.default` cache pool, which uses the `cache.adapter.filesystem` adapter, with `tags` enabled.
+
+::: tip It is recommended to use tag-aware cache adapter!
+The built-in [cache persistence clearer](#persistence-clearers) requires tag-aware cache to clear persistence data.
+:::
+
+### Creating custom adapters
+
+To create a custom adapter, create a class that implements `PersistenceAdapterInterface`.
+
+```php
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface;
+
+class DatabasePersistenceAdapter implements PersistenceAdapterInterface
+{
+ public function __construct(
+ private EntityManagerInterface $entityManager,
+ private string $prefix,
+ ) {
+ }
+
+ public function read(DataTableInterface $dataTable, PersistenceSubjectInterface $subject): mixed
+ {
+ // ...
+ }
+
+ public function write(DataTableInterface $dataTable, PersistenceSubjectInterface $subject, mixed $data): void
+ {
+ // ...
+ }
+}
+```
+
+
+
+Recommended namespace for the column type classes is `App\DataTable\Persistence\`.
+
+
+
+The recommended way of creating those classes is accepting a `prefix` argument in the constructor.
+This prefix will be different for each feature, for example, personalization persistence will use `personalization` prefix.
+
+Now, register it in the container as an abstract service:
+
+::: code-group
+```yaml [YAML]
+services:
+ app.data_table.persistence.database:
+ class: App\DataTable\Persistence\DatabasePersistenceAdapter
+ abstract: true
+ arguments:
+ - '@doctrine.orm.entity_manager'
+```
+
+```php [PHP]
+use App\DataTable\Persistence\DatabasePersistenceAdapter;
+use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
+
+return static function (ContainerConfigurator $configurator) {
+ $configurator->services()
+ ->set('app.data_table.persistence.database', DatabasePersistenceAdapter::class)
+ ->args([service('doctrine.orm.entity_manager')])
+ ->abstract()
+ ;
+```
+:::
+
+Now, create as many adapters as you need, based on the abstract definition.
+For example, let's create an adapter for personalization feature, using the `personalization` prefix:
+
+::: code-group
+```yaml [YAML]
+services:
+ app.data_table.personalization.persistence.database:
+ parent: app.data_table.persistence.database
+ arguments:
+ $prefix: personalization
+ tags:
+ - { name: kreyu_data_table.proxy_query.factory }
+```
+
+```php [PHP]
+use App\DataTable\Persistence\DatabasePersistenceAdapter;
+use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
+
+return static function (ContainerConfigurator $configurator) {
+ $configurator->services()
+ ->set('app.data_table.personalization.persistence.database')
+ ->parent('app.data_table.persistence.database')
+ ->arg('$prefix', 'personalization')
+ ;
+```
+:::
+
+The data tables can now be configured to use the new persistence adapter for the personalization feature:
+
+::: code-group
+```yaml [Globally (YAML)]
+kreyu_data_table:
+ defaults:
+ personalization:
+ persistence_adapter: app.data_table.personalization.persistence.database
+```
+
+```php [Globally (PHP)]
+use Symfony\Config\KreyuDataTableConfig;
+
+return static function (KreyuDataTableConfig $config) {
+ $config->defaults()
+ ->personalization()
+ ->persistenceAdapter('app.data_table.personalization.persistence.database')
+ ;
+};
+```
+
+```php [For data table type]
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function __construct(
+ #[Autowire(service: 'app.data_table.personalization.persistence.database')]
+ private PersistenceAdapterInterface $persistenceAdapter,
+ ) {
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'personalization_persistence_adapter' => $this->persistenceAdapter,
+ ]);
+ }
+}
+```
+
+```php [For specific data table]
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function __construct(
+ #[Autowire(service: 'app.data_table.personalization.persistence.database')]
+ private PersistenceAdapterInterface $persistenceAdapter,
+ ) {
+ }
+
+ public function index()
+ {
+ $dataTable = $this->createDataTable(
+ type: ProductDataTableType::class,
+ query: $query,
+ options: [
+ 'personalization_persistence_adapter' => $this->persistenceAdapter,
+ ],
+ );
+ }
+}
+```
+:::
+
+## Persistence subjects
+
+Persistence subject can be any object that implements `PersistenceSubjectInterface`.
+
+The value returned in the `getDataTablePersistenceIdentifier()` is used in
+[persistence adapters](#persistence-adapters) to associate persistent data with the subject.
+
+### Subject providers
+
+Persistence subject providers are classes that allow retrieving the [persistence subjects](#persistence-subjects).
+Those classes contain `provide` method, that should return the subject, or throw an `PersistenceSubjectNotFoundException`.
+
+### Built-in token storage subject provider
+
+The bundle has a built-in token storage subject provider, which uses the [Symfony Security component](https://symfony.com/doc/current/security.html) to retrieve currently logged-in user.
+This provider uses the [UserInterface](https://github.com/symfony/symfony/blob/6.4/src/Symfony/Component/Security/Core/User/UserInterface.php) `getUserIdentifier()`
+method to retrieve the persistence identifier.
+
+::: danger The persistence identifier must be **unique** per user!
+Otherwise, multiple users will override each other's data, like applied filters or current page.
+:::
+
+You can manually provide the persistence identifier by implementing the `PersistenceSubjectInterface` interface on your User entity used by the security:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectInterface;
+
+class User implements PersistenceSubjectInterface
+{
+ private Uuid $uuid;
+
+ public function getDataTablePersistenceIdentifier(): string
+ {
+ return (string) $this->uuid;
+ }
+}
+```
+
+::: tip Persistence "cache tag contains reserved characters" error?
+If your User entity returns email address in `getUserIdentifier()` method, this creates a conflict
+when using the [cache adapter](#built-in-cache-adapter), because the `@` character cannot be used as a cache key.
+
+For more information, see [troubleshooting section](../troubleshooting.md#persistence-cache-tag-contains-reserved-characters-error).
+:::
+
+### Creating custom subject providers
+
+To create a custom subject provider, create a class that implements `PersistenceSubjectProviderInterface`:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface;
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectInterface;
+
+class CustomPersistenceSubjectProvider implements PersistenceSubjectProviderInterface
+{
+ public function provide(): PersistenceSubjectInterface
+ {
+ // ...
+ }
+}
+```
+
+Subject providers must be registered as services and tagged with the `kreyu_data_table.persistence.subject_provider` tag.
+If you're using the [default services.yaml configuration](https://symfony.com/doc/current/service_container.html#service-container-services-load-example),
+this is already done for you, thanks to [autoconfiguration](https://symfony.com/doc/current/service_container.html#services-autoconfigure).
+
+When using the default container configuration, that provider should be ready to use.
+If not, consider tagging this class as `kreyu_data_table.persistence.subject_provider`:
+
+::: code-group
+```yaml [YAML]
+services:
+ app.data_table.persistence.subject_provider.custom:
+ class: App\DataTable\Persistence\CustomPersistenceSubjectProvider
+ tags:
+ - { name: kreyu_data_table.persistence.subject_provider }
+```
+
+```php [PHP]
+use App\DataTable\Persistence\CustomPersistenceSubjectProvider;
+use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
+
+return static function (ContainerConfigurator $configurator) {
+ $configurator->services()
+ ->set('app.data_table.persistence.database', CustomPersistenceSubjectProvider::class)
+ ->tag('kreyu_data_table.persistence.subject_provider')
+ ;
+}
+```
+:::
+
+The data tables can now be configured to use the new persistence subject provider for any feature.
+For example, for personalization feature:
+
+::: code-group
+```yaml [Globally (YAML)]
+kreyu_data_table:
+ defaults:
+ personalization:
+ persistence_subject_provider: app.data_table.persistence.subject_provider.custom
+```
+
+```php [Globally (PHP)]
+use Symfony\Config\KreyuDataTableConfig;
+
+return static function (KreyuDataTableConfig $config) {
+ $config->defaults()
+ ->personalization()
+ ->persistenceSubjectProvider('app.data_table.persistence.subject_provider.custom')
+ ;
+};
+```
+
+```php [For data table type]
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function __construct(
+ #[Autowire(service: 'app.data_table.persistence.subject_provider.custom')]
+ private PersistenceSubjectProviderInterface $persistenceSubjectProvider,
+ ) {
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'personalization_persistence_subject_provider' => $this->persistenceSubjectProvider,
+ ]);
+ }
+}
+```
+
+```php [For specific data table]
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function __construct(
+ #[Autowire(service: 'app.data_table.personalization.persistence.database')]
+ private PersistenceAdapterInterface $persistenceAdapter,
+ ) {
+ }
+
+ public function index()
+ {
+ $dataTable = $this->createDataTable(
+ type: ProductDataTableType::class,
+ query: $query,
+ options: [
+ 'personalization_persistence_adapter' => $this->persistenceAdapter,
+ ],
+ );
+ }
+}
+```
+:::
+
+## Persistence clearers
+
+Persistence data can be cleared using persistence clearers, which are classes that implement [`PersistenceClearerInterface`](#).
+Those classes contain a `clear()` method, which accepts a [persistence subject](#persistence-subjects) as an argument.
+
+Because the bundle has a built-in cache adapter, it also provides a cache persistence clearer:
+
+```bash
+$ bin/console debug:container kreyu_data_table.persistence.clearer.cache
+```
+
+Let's assume, that the user has a "Clear data table persistence" button, somewhere on the "settings" page.
+Handling this button in controller is very straightforward:
+
+```php
+use App\Entity\User;
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceClearerInterface;
+use Symfony\Component\Routing\Annotation\Route;
+
+class UserController
+{
+ #[Route('/users/{id}/clear-persistence')]
+ public function clearPersistence(User $user, PersistenceClearerInterface $persistenceClearer)
+ {
+ $persistenceClearer->clear($user);
+
+ // Flash with success, redirect, etc...
+ }
+}
+```
diff --git a/docs/src/docs/features/personalization.md b/docs/src/docs/features/personalization.md
new file mode 100644
index 00000000..9b4619e4
--- /dev/null
+++ b/docs/src/docs/features/personalization.md
@@ -0,0 +1,263 @@
+# Personalization
+
+The data tables can be _personalized_, which can be helpful when working with many columns, by giving the user ability to:
+
+- set the priority (order) of the columns;
+- show or hide specific columns;
+
+::: details Screenshots
+![Personalization modal with Tabler theme](/personalization_modal.png)
+:::
+
+[[toc]]
+
+## Prerequisites
+
+To begin with, make sure the [Symfony UX integration is enabled](../installation.md#enable-the-symfony-ux-integration).
+Then, enable the **personalization** controller in your `assets/controllers.json` file:
+
+```json
+{
+ "controllers": {
+ "@kreyu/data-table-bundle": {
+ "personalization": {
+ "enabled": true
+ }
+ }
+ }
+}
+```
+
+## Toggling the feature
+
+By default, the personalization feature is **disabled** for every data table.
+
+You can change this setting globally using the package configuration file, or use `personalization_enabled` option:
+
+::: code-group
+```yaml [Globally (YAML)]
+kreyu_data_table:
+ defaults:
+ personalization:
+ enabled: true
+```
+
+```php [Globally (PHP)]
+use Symfony\Config\KreyuDataTableConfig;
+
+return static function (KreyuDataTableConfig $config) {
+ $defaults = $config->defaults();
+ $defaults->personalization()->enabled(true);
+};
+```
+
+```php [For data table type]
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'personalization_enabled' => true,
+ ]);
+ }
+}
+```
+
+```php [For specific data table]
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function index()
+ {
+ $dataTable = $this->createDataTable(
+ type: ProductDataTableType::class,
+ query: $query,
+ options: [
+ 'personalization_enabled' => true,
+ ],
+ );
+ }
+}
+```
+:::
+
+## Saving applied personalization
+
+By default, the personalization feature [persistence](persistence.md) is **disabled** for every data table.
+
+You can configure the [persistence](persistence.md) globally using the package configuration file, or its related options:
+
+::: code-group
+```yaml [Globally (YAML)]
+kreyu_data_table:
+ defaults:
+ personalization:
+ persistence_enabled: true
+ # if persistence is enabled and symfony/cache is installed, null otherwise
+ persistence_adapter: kreyu_data_table.sorting.persistence.adapter.cache
+ # if persistence is enabled and symfony/security-bundle is installed, null otherwise
+ persistence_subject_provider: kreyu_data_table.persistence.subject_provider.token_storage
+```
+
+```php [Globally (PHP)]
+use Symfony\Config\KreyuDataTableConfig;
+
+return static function (KreyuDataTableConfig $config) {
+ $defaults = $config->defaults();
+ $defaults->personalization()
+ ->persistenceEnabled(true)
+ // if persistence is enabled and symfony/cache is installed, null otherwise
+ ->persistenceAdapter('kreyu_data_table.sorting.persistence.adapter.cache')
+ // if persistence is enabled and symfony/security-bundle is installed, null otherwise
+ ->persistenceSubjectProvider('kreyu_data_table.persistence.subject_provider.token_storage')
+ ;
+};
+```
+
+```php [For data table type]
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface;
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function __construct(
+ private PersistenceAdapterInterface $persistenceAdapter,
+ private PersistenceSubjectProviderInterface $persistenceSubjectProvider,
+ ) {
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'personalization_persistence_enabled' => true,
+ 'personalization_persistence_adapter' => $this->persistenceAdapter,
+ 'personalization_persistence_subject_provider' => $this->persistenceSubjectProvider,
+ ]);
+ }
+}
+```
+
+```php [For specific data table]
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface;
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function __construct(
+ private PersistenceAdapterInterface $persistenceAdapter,
+ private PersistenceSubjectProviderInterface $persistenceSubjectProvider,
+ ) {
+ }
+
+ public function index()
+ {
+ $dataTable = $this->createDataTable(
+ type: ProductDataTableType::class,
+ query: $query,
+ options: [
+ 'personalization_persistence_enabled' => true,
+ 'personalization_persistence_adapter' => $this->persistenceAdapter,
+ 'personalization_persistence_subject_provider' => $this->persistenceSubjectProvider,
+ ],
+ );
+ }
+}
+```
+:::
+
+## Default personalization
+
+There are two ways to configure the default personalization data for the data table:
+
+- using the columns [`priority`](../../reference/types/column/column.md#priority), [`visible`](../../reference/types/column/column.md#visible) and [`personalizable`](../../reference/types/column/column.md#personalizable) options (recommended);
+- using the data table builder's `setDefaultPersonalizationData()` method;
+
+```php src/DataTable/Type/ProductDataTableType.php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\Personalization\PersonalizationData;
+use Kreyu\Bundle\DataTableBundle\Personalization\PersonalizationColumnData;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ // using the columns options:
+ $builder
+ ->addColumn('id', NumberColumnType::class, [
+ 'priority' => -1,
+ ])
+ ->addColumn('name', TextColumnType::class, [
+ 'visible' => false,
+ ])
+ ->addColumn('createdAt', DateTimeColumnType::class, [
+ 'personalizable' => false,
+ ])
+ ;
+
+ // or using the data table builder's method:
+ $builder->setDefaultPersonalizationData(new PersonalizationData([
+ new PersonalizationColumnData(name: 'id', priority: -1),
+ new PersonalizationColumnData(name: 'name', visible: false),
+ ]));
+
+ // or by creating the personalization data from an array:
+ $builder->setDefaultPersonalizationData(PersonalizationData::fromArray([
+ // each entry default values: name = from key, priority = 0, visible = false
+ 'id' => ['priority' => -1],
+ 'name' => ['visible' => false],
+ ]));
+ }
+}
+```
+
+## Events
+
+The following events are dispatched when `personalize()` method of the [`DataTableInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/DataTableInterface.php) is called:
+
+::: info PRE_PERSONALIZE
+Dispatched before the personalization data is applied to the data table.
+Can be used to modify the personalization data, e.g. to dynamically specify priority or visibility of the columns.
+
+**See**: [`DataTableEvents::PRE_PERSONALIZE`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php)
+:::
+
+::: info POST_PERSONALIZE
+Dispatched after the personalization data is applied to the data table and saved if the personalization persistence is enabled.
+Can be used to execute additional logic after the personalization is applied.
+
+**See**: [`DataTableEvents::POST_PERSONALIZE`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php)
+:::
+
+The dispatched events are instance of the [`DataTablePersonalizationEvent`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTablePersonalizationEvent.php):
+
+```php
+use Kreyu\Bundle\DataTableBundle\Event\DataTablePersonalizationEvent;
+
+class DataTablePersonalizationListener
+{
+ public function __invoke(DataTablePersonalizationEvent $event): void
+ {
+ $dataTable = $event->getDataTable();
+ $personalizationData = $event->getPersonalizationData();
+
+ // for example, modify the personalization data, then save it in the event
+ $event->setPersonalizationData($personalizationData);
+ }
+}
+```
\ No newline at end of file
diff --git a/docs/src/docs/features/sorting.md b/docs/src/docs/features/sorting.md
new file mode 100644
index 00000000..e38ec251
--- /dev/null
+++ b/docs/src/docs/features/sorting.md
@@ -0,0 +1,297 @@
+# Sorting
+
+The data tables can be _sorted_ by any defined [column](../components/columns.md).
+
+[[toc]]
+
+## Toggling the feature
+
+By default, the sorting feature is **enabled** for every data table.
+
+You can change this setting globally using the package configuration file, or use `sorting_enabled` option:
+
+::: code-group
+```yaml [Globally (YAML)]
+kreyu_data_table:
+ defaults:
+ sorting:
+ enabled: true
+```
+
+```php [Globally (PHP)]
+use Symfony\Config\KreyuDataTableConfig;
+
+return static function (KreyuDataTableConfig $config) {
+ $defaults = $config->defaults();
+ $defaults->sorting()->enabled(true);
+};
+```
+
+```php [For data table type]
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'sorting_enabled' => true,
+ ]);
+ }
+}
+```
+
+```php [For specific data table]
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function index()
+ {
+ $dataTable = $this->createDataTable(
+ type: ProductDataTableType::class,
+ query: $query,
+ options: [
+ 'sorting_enabled' => true,
+ ],
+ );
+ }
+}
+```
+:::
+
+::: tip Enabling the feature does not mean that any column will be sortable by itself.
+By default, columns **are not** sortable.
+:::
+
+## Making the columns sortable
+
+To make any column sortable, use its `sort` option:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Column\Type\NumberColumnType;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addColumn('id', NumberColumnType::class, [
+ 'sort' => true,
+ ])
+ ;
+ }
+}
+```
+
+The bundle will use the column name as the path to perform sorting on.
+However, if the path is different from the column name (for example, to display "category", but sort by the "category name"), provide it using the same `sort` option:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addColumn('category', TextColumnType::class, [
+ 'sort' => 'category.name',
+ ])
+ ;
+ }
+}
+```
+
+If the column should be sorted by multiple database columns (for example, to sort by amount and currency at the same time),
+when using the Doctrine ORM, provide a DQL expression as a sort property path:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addColumn('amount', TextColumnType::class, [
+ 'sort' => 'CONCAT(product.amount, product.currency)',
+ ])
+ ;
+ }
+}
+```
+
+## Saving applied sorting
+
+By default, the sorting feature [persistence](persistence.md) is **disabled** for every data table.
+
+You can configure the [persistence](persistence.md) globally using the package configuration file, or its related options:
+
+::: code-group
+```yaml [Globally (YAML)]
+kreyu_data_table:
+ defaults:
+ sorting:
+ persistence_enabled: true
+ # if persistence is enabled and symfony/cache is installed, null otherwise
+ persistence_adapter: kreyu_data_table.sorting.persistence.adapter.cache
+ # if persistence is enabled and symfony/security-bundle is installed, null otherwise
+ persistence_subject_provider: kreyu_data_table.persistence.subject_provider.token_storage
+```
+
+```php [Globally (PHP)]
+use Symfony\Config\KreyuDataTableConfig;
+
+return static function (KreyuDataTableConfig $config) {
+ $defaults = $config->defaults();
+ $defaults->sorting()
+ ->persistenceEnabled(true)
+ // if persistence is enabled and symfony/cache is installed, null otherwise
+ ->persistenceAdapter('kreyu_data_table.sorting.persistence.adapter.cache')
+ // if persistence is enabled and symfony/security-bundle is installed, null otherwise
+ ->persistenceSubjectProvider('kreyu_data_table.persistence.subject_provider.token_storage')
+ ;
+};
+```
+
+```php [For data table type]
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface;
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function __construct(
+ #[Autowire(service: 'kreyu_data_table.filtration.persistence.adapter.cache')]
+ private PersistenceAdapterInterface $persistenceAdapter,
+ #[Autowire(service: 'kreyu_data_table.persistence.subject_provider.token_storage')]
+ private PersistenceSubjectProviderInterface $persistenceSubjectProvider,
+ ) {
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'sorting_persistence_enabled' => true,
+ 'sorting_persistence_adapter' => $this->persistenceAdapter,
+ 'sorting_persistence_subject_provider' => $this->persistenceSubjectProvider,
+ ]);
+ }
+}
+```
+
+```php [For specific data table]
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface;
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function __construct(
+ #[Autowire(service: 'kreyu_data_table.filtration.persistence.adapter.cache')]
+ private PersistenceAdapterInterface $persistenceAdapter,
+ #[Autowire(service: 'kreyu_data_table.persistence.subject_provider.token_storage')]
+ private PersistenceSubjectProviderInterface $persistenceSubjectProvider,
+ ) {
+ }
+
+ public function index()
+ {
+ $dataTable = $this->createDataTable(
+ type: ProductDataTableType::class,
+ query: $query,
+ options: [
+ 'sorting_persistence_enabled' => true,
+ 'sorting_persistence_adapter' => $this->persistenceAdapter,
+ 'sorting_persistence_subject_provider' => $this->persistenceSubjectProvider,
+ ],
+ );
+ }
+}
+```
+:::
+
+## Default sorting
+
+The default sorting data can be overridden using the data table builder's `setDefaultSortingData()` method:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\Sorting\SortingData;
+use Kreyu\Bundle\DataTableBundle\Sorting\SortingColumnData;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder->setDefaultSortingData(new SortingData([
+ new SortingColumnData('id', 'desc'),
+ ]));
+
+ // or by creating the sorting data from an array:
+ $builder->setDefaultSortingData(SortingData::fromArray([
+ 'id' => 'desc',
+ ]));
+ }
+}
+```
+
+::: tip The initial sorting can be performed on multiple columns!
+Although, with built-in themes, the user can perform sorting only by a single column.
+:::
+
+## Events
+
+The following events are dispatched when `sort()` method of the [`DataTableInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/DataTableInterface.php) is called:
+
+::: info PRE_SORT
+Dispatched before the sorting data is applied to the query.
+Can be used to modify the sorting data, e.g. to force sorting by additional column.
+
+**See**: [`DataTableEvents::PRE_SORT`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php)
+:::
+
+::: info POST_SORT
+Dispatched after the sorting data is applied to the query and saved if the sorting persistence is enabled;
+Can be used to execute additional logic after the sorting is applied.
+
+**See**: [`DataTableEvents::POST_SORT`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php)
+:::
+
+The dispatched events are instance of the [`DataTableSortingEvent`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableSortingEvent.php):
+
+```php
+use Kreyu\Bundle\DataTableBundle\Event\DataTableSortingEvent;
+
+class DataTableSortingListener
+{
+ public function __invoke(DataTableSortingEvent $event): void
+ {
+ $dataTable = $event->getDataTable();
+ $sortingData = $event->getSortingData();
+
+ // for example, modify the sorting data, then save it in the event
+ $event->setSortingData($sortingData);
+ }
+}
+```
\ No newline at end of file
diff --git a/docs/src/docs/features/theming.md b/docs/src/docs/features/theming.md
new file mode 100644
index 00000000..d55a7b60
--- /dev/null
+++ b/docs/src/docs/features/theming.md
@@ -0,0 +1,163 @@
+# Theming
+
+Every HTML part of this bundle can be customized using [Twig](https://twig.symfony.com/) themes.
+
+[[toc]]
+
+## Built-in themes
+
+The following themes are natively available in the bundle:
+
+- [`@KreyuDataTable/themes/bootstrap_5.html.twig`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Resources/views/themes/bootstrap_5.html.twig) - integrates [Bootstrap 5](https://getbootstrap.com/docs/5.0/);
+- [`@KreyuDataTable/themes/tabler.html.twig`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Resources/views/themes/tabler.html.twig) - integrates [Tabler UI Kit](https://tabler.io/);
+- [`@KreyuDataTable/themes/base.html.twig`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Resources/views/themes/base.html.twig) - base HTML template;
+
+By default, the [`@KreyuDataTable/themes/base.html.twig`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Resources/views/themes/base.html.twig) theme is used.
+
+## Selecting a theme
+
+To select a theme, use `themes` option.
+
+For example, in order to use the [Bootstrap 5](https://getbootstrap.com/docs/5.0/) theme:
+
+::: code-group
+```yaml [Globally (YAML)]
+kreyu_data_table:
+ defaults:
+ themes:
+ - '@KreyuDataTable/themes/bootstrap_5.html.twig'
+```
+
+```php [Globally (PHP)]
+use Symfony\Config\KreyuDataTableConfig;
+
+return static function (KreyuDataTableConfig $config) {
+ $config->defaults()->themes([
+ '@KreyuDataTable/themes/bootstrap_5.html.twig',
+ ]);
+};
+```
+
+```php [For data table type]
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'themes' => [
+ '@KreyuDataTable/themes/bootstrap_5.html.twig',
+ ],
+ ]);
+ }
+}
+```
+
+```php [For specific data table]
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function index()
+ {
+ $dataTable = $this->createDataTable(
+ type: ProductDataTableType::class,
+ query: $query,
+ options: [
+ 'themes' => [
+ '@KreyuDataTable/themes/bootstrap_5.html.twig',
+ ],
+ ],
+ );
+ }
+}
+```
+:::
+
+## Customizing existing theme
+
+To customize existing theme, you can either:
+
+- create a template that extends one of the built-in themes;
+- create a template that [overrides the built-in theme](https://symfony.com/doc/current/bundles/override.html#templates);
+- create a template from scratch;
+
+Because `themes` configuration option accepts an array of themes,
+you can provide your own theme with only a fraction of Twig blocks,
+using the built-in themes as a fallback, for example:
+
+```twig
+{# templates/data_table/theme.html.twig #}
+{% block column_boolean_value %}
+ {# ... #}
+{% endblock %}
+```
+
+::: code-group
+```yaml [Globally (YAML)]
+kreyu_data_table:
+ defaults:
+ themes:
+ - '@KreyuDataTable/themes/bootstrap_5.html.twig'
+```
+
+```php [Globally (PHP)]
+use Symfony\Config\KreyuDataTableConfig;
+
+return static function (KreyuDataTableConfig $config) {
+ $config->defaults()->themes([
+ 'templates/data_table/theme.html.twig',
+ '@KreyuDataTable/themes/bootstrap_5.html.twig',
+ ]);
+};
+```
+
+```php [For data table type]
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'themes' => [
+ 'templates/data_table/theme.html.twig',
+ '@KreyuDataTable/themes/bootstrap_5.html.twig',
+ ],
+ ]);
+ }
+}
+```
+
+```php [For specific data table]
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function index()
+ {
+ $dataTable = $this->createDataTable(
+ type: ProductDataTableType::class,
+ query: $query,
+ options: [
+ 'themes' => [
+ 'templates/data_table/theme.html.twig',
+ '@KreyuDataTable/themes/bootstrap_5.html.twig',
+ ],
+ ],
+ );
+ }
+}
+```
+:::
\ No newline at end of file
diff --git a/docs/src/docs/installation.md b/docs/src/docs/installation.md
new file mode 100644
index 00000000..77fa5f4c
--- /dev/null
+++ b/docs/src/docs/installation.md
@@ -0,0 +1,81 @@
+# Installation
+
+This bundle can be installed at any moment during a project’s lifecycle.
+
+[[toc]]
+
+## Prerequisites
+
+- PHP version 8.1 or higher
+- Symfony version 6.0 or higher
+
+## Download the bundle
+
+Use [Composer](https://getcomposer.org/) to install the bundle:
+
+```shell
+composer require kreyu/data-table-bundle 0.16.*
+```
+
+::: danger This bundle is not production ready!
+It is recommended to lock the minor version, as minor versions can provide breaking changes until the stable release!
+:::
+
+## Enable the bundles
+
+Enable the bundles by adding them to the `config/bundles.php` file of your project:
+
+```php
+return [
+ // ...
+ Kreyu\Bundle\DataTableBundle\KreyuDataTableBundle::class => ['all' => true],
+];
+```
+
+## Enable the Stimulus controllers
+
+This bundle provides front-end scripts created using the [Stimulus JavaScript framework](https://stimulus.hotwired.dev/).
+To begin with, make sure your application uses the [Symfony Stimulus Bridge](https://github.com/symfony/stimulus-bridge).
+
+Then, add `@kreyu/data-table-bundle` dependency to your `package.json` file:
+
+```json
+{
+ "devDependencies": {
+ "@kreyu/data-table-bundle": "file:vendor/kreyu/data-table-bundle/assets"
+ }
+}
+```
+
+Now, add `@kreyu/data-table-bundle` controllers to your `assets/controllers.json` file:
+
+```json
+{
+ "controllers": {
+ "@kreyu/data-table-bundle": {
+ "personalization": {
+ "enabled": true
+ },
+ "batch": {
+ "enabled": true
+ }
+ }
+ }
+}
+```
+
+## Rebuild assets
+
+If you're using [AssetMapper](https://symfony.com/doc/current/frontend.html#assetmapper-recommended), you're good to go. Otherwise, run following command:
+
+::: code-group
+
+```shell [yarn]
+yarn install --force && yarn watch
+```
+
+```shell [npm]
+npm install --force && npm run watch
+```
+
+:::
diff --git a/docs/src/docs/integrations/doctrine-orm/events.md b/docs/src/docs/integrations/doctrine-orm/events.md
new file mode 100644
index 00000000..c188f9f0
--- /dev/null
+++ b/docs/src/docs/integrations/doctrine-orm/events.md
@@ -0,0 +1,75 @@
+# Events
+
+The Doctrine ORM filter handler dispatches two events, which can be used to modify the query parameters and the expression.
+
+## PreSetParametersEvent
+
+The [PreSetParametersEvent](../src/Event/PreSetParametersEvent.php) is dispatched before `$queryBuilder->setParameter()`
+is called for every parameter required by the filter. It can be used to modify the parameters, before they are passed to the query builder.
+
+```php
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Event\DoctrineOrmFilterEvents;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Event\PreSetParametersEvent;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ // ...
+
+ $builder
+ ->getFilter('name')
+ ->addEventListener(DoctrineOrmFilterEvents::PRE_SET_PARAMETERS, function (PreSetParametersEvent $event) {
+ $filter = $event->getFilter();
+ $data = $event->getData();
+ $query = $event->getQuery();
+ $parameters = $event->getParameters();
+
+ // ...
+
+ $event->setParameters($parameters);
+ });
+ }
+}
+```
+
+## PreApplyExpressionEvent
+
+The [PreApplyExpressionEvent](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Event/PreApplyExpressionEvent.php) is dispatched before `$queryBuilder->andWhere()` is called.
+It can be used to modify the expression before it is passed to the query builder.
+
+::: tip Use [expression transformers](expression-transformers.md) for easier and reusable solution for modifying the expression.
+Those transformers are called by the [ApplyExpressionTransformers](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/EventListener/ApplyExpressionTransformers.php) event subscriber,
+which is automatically used in [DoctrineOrmFilterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/Type/DoctrineOrmFilterType.php) filter type, as well as
+every filter type that uses it as a parent.
+:::
+
+```php
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Event\DoctrineOrmFilterEvents;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Event\PreApplyExpressionEvent;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ // ...
+
+ $builder
+ ->getFilter('name')
+ ->addEventListener(DoctrineOrmFilterEvents::PRE_APPLY_EXPRESSION, function (PreApplyExpressionEvent $event) {
+ $filter = $event->getFilter();
+ $data = $event->getData();
+ $query = $event->getQuery();
+ $expression = $event->getExpression();
+
+ // ...
+
+ $event->setExpression($expression);
+ });
+ }
+}
+```
\ No newline at end of file
diff --git a/docs/src/docs/integrations/doctrine-orm/expression-transformers.md b/docs/src/docs/integrations/doctrine-orm/expression-transformers.md
new file mode 100644
index 00000000..3f3964e1
--- /dev/null
+++ b/docs/src/docs/integrations/doctrine-orm/expression-transformers.md
@@ -0,0 +1,241 @@
+# Expression transformers
+
+Expression transformers provide a way to extend Doctrine DQL expressions before they are executed by a filter handler.
+
+[[toc]]
+
+## Built-in expression transformers
+
+- `TrimExpressionTransformer` - wraps the expression in the `TRIM()` function
+- `LowerExpressionTransformer` - wraps the expression in the `LOWER()` function
+- `UpperExpressionTransformer` - wraps the expression in the `UPPER()` function
+- `CallbackExpressionTransformer` - allows transforming the expression using the callback function
+
+The expression transformers can be passed using the `expression_transformers` option:
+
+```php
+use App\DataTable\Filter\ExpressionTransformer\UnaccentExpressionTransformer;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\ExpressionTransformer\LowerExpressionTransformer;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\ExpressionTransformer\TrimExpressionTransformer;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addFilter('name', TextFilterType::class, [
+ 'expression_transformers' => [
+ new LowerExpressionTransformer(),
+ new TrimExpressionTransformer(),
+ ],
+ ])
+ ;
+ }
+}
+```
+
+
+
+The transformers are called in the same order as they are passed.
+
+
+
+For easier usage, some of the built-in transformers can be enabled using the `trim`, `lower` and `upper` filter options:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addFilter('name', TextFilterType::class, [
+ 'trim' => true,
+ 'lower' => true,
+ 'upper' => true,
+ ])
+ ;
+ }
+}
+```
+
+
+
+When using the `trim`, `lower` or `upper` options, their transformers are called **before** the `expression_transformers` ones.
+
+
+
+## Creating a custom expression transformer
+
+To create a custom expression transformer, create a new class that implements `ExpressionTransformerInterface`:
+
+```php
+namespace App\DataTable\Filter\ExpressionTransformer;
+
+use Doctrine\ORM\Query\Expr;
+use Doctrine\ORM\Query\Expr\Comparison;
+use Kreyu\Bundle\DataTableBundle\Exception\UnexpectedTypeException;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\ExpressionTransformer\ExpressionTransformerInterface;
+
+class UnaccentExpressionTransformer implements ExpressionTransformerInterface
+{
+ public function transform(mixed $expression): mixed
+ {
+ if (!$expression instanceof Comparison) {
+ throw new UnexpectedTypeException($expression, Comparison::class);
+ }
+
+ $leftExpr = sprintf('UNACCENT(%s)', (string) $expression->getLeftExpr());
+ $rightExpr = sprintf('UNACCENT(%s)', (string) $expression->getRightExpr());
+
+ // or use expression API:
+ //
+ // $leftExpr = new Expr\Func('UNACCENT', $expression->getLeftExpr());
+ // $rightExpr = new Expr\Func('UNACCENT', $expression->getRightExpr());
+
+ return new Comparison($leftExpr, $expression->getOperator(), $rightExpr);
+ }
+}
+```
+
+If you're sure that the expression transformer requires the expression to be a comparison (it will be in most cases),
+you can extend the `AbstractComparisonExpressionTransformer` class which simplifies the definition:
+
+```php
+namespace App\DataTable\Filter\ExpressionTransformer;
+
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\ExpressionTransformer\AbstractComparisonExpressionTransformer;
+
+class UnaccentExpressionTransformer extends AbstractComparisonExpressionTransformer
+{
+ protected function transformLeftExpr(mixed $leftExpr): mixed
+ {
+ return sprintf('UNACCENT(%s)', (string) $leftExpr);
+
+ // or use expression API:
+ //
+ // return new Expr\Func('UNACCENT', $leftExpr);
+ }
+
+ protected function transformRightExpr(mixed $rightExpr): mixed
+ {
+ return sprintf('UNACCENT(%s)', (string) $rightExpr);
+
+ // or use expression API:
+ //
+ // return new Expr\Func('UNACCENT', $rightExpr);
+ }
+}
+```
+
+The `AbstractComparisonExpressionTransformer` accepts two boolean arguments:
+
+- `transformLeftExpr` - defaults to `true`
+- `transformRightExpr` - defaults to `true`
+
+Thanks to that, the user can specify which side of the expression should be transformed.
+The `transformLeftExpr()` and `transformRightExpr()` methods are called only when necessary. For example:
+
+```php
+$expression = new Expr\Comparison('foo', '=', 'bar');
+
+// LOWER(foo) = LOWER(bar)
+(new LowerExpressionTransformer())
+ ->transform($expression);
+
+// foo = LOWER(bar)
+(new LowerExpressionTransformer(transformLeftExpr: false, transformRightExpr: true))
+ ->transform($expression);
+
+// LOWER(foo) = bar
+(new LowerExpressionTransformer(transformLeftExpr: true, transformRightExpr: false))
+ ->transform($expression);
+```
+
+To use the created expression transformer, pass it as the `expression_transformers` filter type option:
+
+```php
+use App\DataTable\Filter\ExpressionTransformer\UnaccentExpressionTransformer;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addFilter('name', TextFilterType::class, [
+ 'expression_transformers' => [
+ new UnaccentExpressionTransformer(),
+ ],
+ ])
+ ;
+ }
+}
+```
+
+## Adding an option to automatically apply transformer
+
+Following the above example of `UnaccentExpressionTransformer`, let's assume, that we want to create such definition:
+
+```php
+use App\DataTable\Filter\ExpressionTransformer\UnaccentExpressionTransformer;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addFilter('name', TextFilterType::class, [
+ 'unaccent' => true,
+ ])
+ ;
+ }
+}
+```
+
+To achieve that, create a custom filter type extension:
+
+```php
+use App\DataTable\Filter\ExpressionTransformer\UnaccentExpressionTransformer;
+use Kreyu\Bundle\DataTableBundle\Filter\Extension\AbstractFilterTypeExtension;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\DoctrineOrmFilterType;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+use Symfony\Component\OptionsResolver\Options;
+
+class UnaccentFilterTypeExtension extends AbstractFilterTypeExtension
+{
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver
+ ->setDefault('unaccent', false)
+ ->setAllowedTypes('unaccent', 'bool')
+ ->addNormalizer('expression_transformers', function (Options $options, array $expressionTransformers) {
+ if ($options['unaccent']) {
+ $expressionTransformers[] = new UnaccentExpressionTransformer();
+ }
+
+ return $expressionTransformers;
+ })
+ ;
+ }
+
+ public static function getExtendedTypes(): iterable
+ {
+ return [DoctrineOrmFilterType::class];
+ }
+}
+```
+
+The `unaccent` option is now defined, and defaults to `false`. In addition, the options resolver normalizer will automatically push an instance
+of the custom expression transformer to the `expression_transformers` option, but only if the `unaccent` option equals `true`.
diff --git a/docs/src/docs/introduction.md b/docs/src/docs/introduction.md
new file mode 100644
index 00000000..a9d240e2
--- /dev/null
+++ b/docs/src/docs/introduction.md
@@ -0,0 +1,106 @@
+# Introduction
+
+DataTableBundle is a Symfony bundle that aims to help with data tables.
+
+[[toc]]
+
+
+
+Just want to try it out? Skip to the [installation](installation.md).
+
+
+
+## Features
+
+- [Type classes](#similarity-to-form-component) for a class-based configuration, like in a Symfony Form component
+- [Sorting](features/sorting.md), [filtering](features/filtering.md) and [pagination](features/pagination.md) - triforce of the data tables
+- [Personalization](features/personalization.md) where the user decides the order and visibility of columns
+- [Persistence](features/persistence.md) to save applied data (e.g. filters) between requests
+- [Exporting](features/exporting.md) with or without applied pagination, filters and personalization
+- [Theming](features/theming.md) of every part of the bundle using Twig
+- [Data source agnostic](features/extensibility.md) with optional Doctrine ORM integration bundle
+- [Integration with Hotwire Turbo](features/asynchronicity.md) for asynchronicity
+
+## Use cases
+
+If your application uses an admin panel generator, like a SonataAdminBundle or EasyAdminBundle, you won't need this bundle.
+Those generators already cover the definition of data tables in their own way.
+
+However, if your application is complex enough that a simple panel generator is not enough, for example, a very specific B2B or CRM platform,
+you may consider DataTableBundle, which focuses solely on the data tables, like a Form component focuses solely on the forms.
+It can save a lot of time when compared to rendering tables manually (especially with filters), and helps with keeping visual homogeneity.
+
+## Similarity to form component
+
+Everything is designed to be friendly to a Symfony developers that used the Symfony Form component before.
+Data tables and their components - [columns](components/columns.md), [filters](components/filters.md), [actions](components/actions.md) and [exporters](components/exporters.md), are defined using type classes, like a forms:
+
+```php
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addColumn('id', NumberColumnType::class)
+ ->addColumn('name', TextColumnType::class);
+
+ $builder
+ ->addFilter('id', NumberFilterType::class)
+ ->addFilter('name', TextFilterType::class);
+
+ $builder
+ ->addAction('create', ButtonActionType::class)
+ ->addRowAction('update', ButtonActionType::class)
+ ->addBatchAction('delete', ButtonActionType::class);
+
+ $builder
+ ->addExporter('csv', CsvExporterType::class)
+ ->addExporter('xlsx', XlsxExporterType::class);
+ }
+}
+```
+
+Creating the data tables using those type classes may also seem very familiar:
+
+```php
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function index(Request $request): Response
+ {
+ $dataTable = $this->createDataTable(ProductDataTableType::class, $query);
+ $dataTable->handleRequest($request);
+
+ return $this->render('product/index.html.twig', [
+ 'products' => $dataTable->createView(),
+ ])
+ }
+}
+```
+
+Rendering the data table in Twig is as simple as executing a single function:
+
+```twig
+{# templates/product/index.html.twig #}
+
+```
+
+## Recommended practices
+
+When working with Form component, each "Type" refers to the form type.
+
+When working with DataTableBundle, there are many new and different types - data table, column, filter, action and exporter types.
+
+For readability, it is recommended to name form types with `FormType` suffix, instead of a simple `Type`.
+This makes a context of the type class clear:
+
+- `ProductFormType` - defines product form;
+- `ProductDataTableType` - defines product list;
+- `ProductColumnType` - defines product column (if separate definition is needed);
+- `ProductFilterType` - defines product filter (if separate definition is needed);
+- etc.
+
+Also, type extensions - instead of `TypeExtension`, use `FormTypeExtension` suffix.
diff --git a/docs/src/docs/troubleshooting.md b/docs/src/docs/troubleshooting.md
new file mode 100644
index 00000000..e078b5c8
--- /dev/null
+++ b/docs/src/docs/troubleshooting.md
@@ -0,0 +1,181 @@
+# Troubleshooting
+
+This section covers common problems and how to fix them.
+
+[[toc]]
+
+## Pagination is enabled but its controls are not visible
+
+By default, every data table has pagination feature _enabled_.
+However, if you don't see the pagination controls, make sure your data table has enough records!
+
+With default pagination data, every page contains 25 records.
+Built-in themes display pagination controls only when the data table contains more than one page.
+Also, remember that you can [change the default pagination data](features/pagination.md#default-pagination), reducing the per-page limit.
+
+For more information, consider reading:
+
+- Features › Pagination › [Toggling the feature](features/pagination.md#toggling-the-feature)
+- Features › Pagination › [Default pagination](features/pagination.md#default-pagination)
+
+## Sorting is enabled but columns are not sortable
+
+Enabling the sorting feature for the data table does not mean that any column will be sortable by itself.
+By default, columns **are not** sortable. To make a column sortable, use its `sort` option.
+
+For more information, consider reading:
+
+- Features › Sorting › [Making the columns sortable](features/sorting.md#making-the-columns-sortable)
+
+## Exporting is enabled but exported files are empty
+
+Enabling the exporting feature for the data table does not mean that any column will be exportable by itself.
+By default, columns **are not** sortable. To make a column exportable, use its `export` option.
+
+For more information, consider reading:
+
+- Features › Exporting › [Making the columns exportable](features/exporting.md#making-the-columns-exportable)
+
+## Data table features are refreshing the page but not working
+
+If, for example, a data table is rendered properly, but:
+- clicking on pagination,
+- changing sort order,
+- applying filters,
+- etc.
+
+refreshes the page but does nothing else, make sure you handled the request using the `handleRequest()` method:
+
+```php
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\Request;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function index(Request $request)
+ {
+ $dataTable = $this->createDataTable(ProductDataTableType::class);
+ $dataTable->handleRequest($request);
+ }
+}
+```
+
+## The N+1 problems
+
+This section covers common issues with N+1 queries when working with Doctrine ORM.
+
+### The N+1 problem with relation columns
+
+When using Doctrine ORM, if your data table contains columns with data from relationship:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addColumn('category.name', TextColumnType::class)
+ ;
+ }
+}
+```
+
+...then, remember to join and select the association to prevent N+1 queries:
+
+```php
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function index(ProductRepository $repository)
+ {
+ $query = $repository->createQueryBuilder('product')
+ ->addSelect('category')
+ ->leftJoin('product.category', 'category')
+ ;
+
+ $dataTable = $this->createDataTable(
+ type: ProductDataTableType::class,
+ query: $query,
+ );
+ }
+}
+```
+
+### The N+1 problem with unused one-to-one relations
+
+If your entity contains a one-to-one relationship that is **not** used in the data table,
+the additional queries will be generated anyway, because the [Doctrine Paginator](https://www.doctrine-project.org/projects/doctrine-orm/en/2.15/tutorials/pagination.html) is **always** loading them.
+To prevent that, add a hint to force a partial load:
+
+```php
+use Doctrine\ORM\Query;
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQuery;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $query = $builder->getQuery();
+
+ if ($query instanceof DoctrineOrmProxyQuery) {
+ $query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true);
+ }
+ }
+}
+```
+
+::: warning
+Forcing a partial load disables lazy loading, therefore, not specifying
+every single association used in the query's `SELECT` will end up in an error.
+:::
+
+For more information, consider reading:
+
+- [[Stack Overflow] Doctrine2 one-to-one relation auto loads on query](https://stackoverflow.com/questions/12362901/doctrine2-one-to-one-relation-auto-loads-on-query/22253783#22253783)
+
+## Persistence "cache tag contains reserved characters" error
+
+When using the default configuration, after enabling the persistence for any feature, it may result in the error:
+
+> Cache tag "kreyu_data_table_persistence_user\@example.com" contains reserved characters "{}()/\\@:".
+
+By default, the bundle is using a cache as a persistence storage, and currently logged-in user as a persistence subject.
+To identify which data belongs to which user, the persistence subject must return a unique identifier.
+To retrieve a unique identifier of a user without additional configuration, a `UserInterface::getUserIdentifier()` method is used.
+Unfortunately, in some applications, it may return something with a reserved character — in case of above error, an email address "user\@example.com".
+
+To prevent that, implement a `PersistenceSubjectInterface` interface on the User object and manually return the **unique** identifier:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectInterface;
+use Symfony\Component\Security\Core\User\UserInterface;
+
+class User implements UserInterface, PersistenceSubjectInterface
+{
+ private int $id;
+
+ public function getDataTablePersistenceIdentifier(): string
+ {
+ return (string) $this->id;
+ }
+}
+```
+
+For more information, consider reading:
+
+- Features › Persistence › [Subject providers](features/persistence.md#subject-providers)
diff --git a/docs/src/index.md b/docs/src/index.md
new file mode 100644
index 00000000..bdc22f59
--- /dev/null
+++ b/docs/src/index.md
@@ -0,0 +1,45 @@
+---
+# https://vitepress.dev/reference/default-theme-home-page
+layout: home
+
+hero:
+ name: "DataTableBundle"
+ text: for Symfony
+ tagline: Streamlines creation process of the data tables
+ image:
+ src: /logo.png
+ alt: DataTableBundle
+ actions:
+ - theme: brand
+ text: Documentation
+ link: /docs/introduction
+ - theme: alt
+ text: Reference
+ link: /reference/types/data-table
+
+features:
+ - title: Type classes
+ details: Class-based configuration, like in a Symfony Form component
+ - title: Sorting, filtering, pagination
+ details: Classic triforce of the data tables
+ - title: Personalization
+ details: User decides the order and visibility of columns
+ - title: Persistence
+ details: Saving applied data (e.g. filters) between requests
+ - title: Exporting
+ details: Popular formats, with or without applied pagination, filters and personalization - you name it
+ - title: Theming
+ details: Every template part of the bundle is customizable using Twig
+ - title: Data source agnostic
+ details: With Doctrine ORM supported out of the box
+ - title: Asynchronous
+ details: Thanks to integration with Hotwire Turbo, data tables are asynchronous
+---
+
+
+
\ No newline at end of file
diff --git a/docs/src/public/action_confirmation_modal.png b/docs/src/public/action_confirmation_modal.png
new file mode 100644
index 00000000..9168bd1c
Binary files /dev/null and b/docs/src/public/action_confirmation_modal.png differ
diff --git a/docs/src/public/export_modal.png b/docs/src/public/export_modal.png
new file mode 100644
index 00000000..d6bc07b1
Binary files /dev/null and b/docs/src/public/export_modal.png differ
diff --git a/docs/src/public/logo.png b/docs/src/public/logo.png
new file mode 100644
index 00000000..d47d7cd7
Binary files /dev/null and b/docs/src/public/logo.png differ
diff --git a/docs/src/public/personalization_modal.png b/docs/src/public/personalization_modal.png
new file mode 100644
index 00000000..78c0f5de
Binary files /dev/null and b/docs/src/public/personalization_modal.png differ
diff --git a/docs/src/public/search_filter_type.png b/docs/src/public/search_filter_type.png
new file mode 100644
index 00000000..407a19e2
Binary files /dev/null and b/docs/src/public/search_filter_type.png differ
diff --git a/docs/src/reference/configuration.md b/docs/src/reference/configuration.md
new file mode 100644
index 00000000..63aa8cd8
--- /dev/null
+++ b/docs/src/reference/configuration.md
@@ -0,0 +1,132 @@
+# Configuration
+
+This bundle can be configured using the:
+
+- `config/packages/kreyu_data_table.yaml` when using YAML configuration;
+- `config/packages/kreyu_data_table.php` when using PHP configuration;
+
+## Data table builder defaults
+
+You can specify default values applied to **all the data tables** using the `defaults` node.
+
+The defaults are loaded by the [DefaultConfigurationDataTableTypeExtension](https://github.com/Kreyu/data-table-bundle/blob/main/src/Extension/Core/DefaultConfigurationDataTableTypeExtension.php),
+which extends every data table type class with [DataTableType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Type/DataTableType.php) specified as a parent.
+
+The given values represent the default ones, unless specifically stated otherwise:
+
+::: code-group
+```yaml [YAML]
+# config/packages/kreyu_data_table.yaml
+kreyu_data_table:
+ defaults:
+ themes:
+ - '@KreyuDataTable/themes/base.html.twig'
+ column_factory: kreyu_data_table.column.factory
+ request_handler: kreyu_data_table.request_handler.http_foundation
+ sorting:
+ enabled: true
+ persistence_enabled: false
+ # if persistence is enabled and symfony/cache is installed, null otherwise
+ persistence_adapter: kreyu_data_table.sorting.persistence.adapter.cache
+ # if persistence is enabled and symfony/security-bundle is installed, null otherwise
+ persistence_subject_provider: kreyu_data_table.persistence.subject_provider.token_storage
+ pagination:
+ enabled: true
+ persistence_enabled: false
+ # if persistence is enabled and symfony/cache is installed, null otherwise
+ persistence_adapter: kreyu_data_table.pagination.persistence.adapter.cache
+ # if persistence is enabled and symfony/security-bundle is installed, null otherwise
+ persistence_subject_provider: kreyu_data_table.persistence.subject_provider.token_storage
+ filtration:
+ enabled: true
+ persistence_enabled: false
+ # if persistence is enabled and symfony/cache is installed, null otherwise
+ persistence_adapter: kreyu_data_table.filtration.persistence.adapter.cache
+ # if persistence is enabled and symfony/security-bundle is installed, null otherwise
+ persistence_subject_provider: kreyu_data_table.persistence.subject_provider.token_storage
+ form_factory: form.factory
+ filter_factory: kreyu_data_table.filter.factory
+ personalization:
+ enabled: false
+ persistence_enabled: false
+ # if persistence is enabled and symfony/cache is installed, null otherwise
+ persistence_adapter: kreyu_data_table.personalization.persistence.adapter.cache
+ # if persistence is enabled and symfony/security-bundle is installed, null otherwise
+ persistence_subject_provider: kreyu_data_table.persistence.subject_provider.token_storage
+ form_factory: form.factory
+ exporting:
+ enabled: true
+ form_factory: form.factory
+ exporter_factory: kreyu_data_table.exporter.factory
+```
+
+```php [PHP]
+defaults();
+
+ $defaults
+ ->themes([
+ '@KreyuDataTable/themes/base.html.twig'
+ ])
+ ->columnFactory('kreyu_data_table.column.factory')
+ ->requestHandler('kreyu_data_table.request_handler.http_foundation')
+ ;
+
+ $defaults->sorting()
+ ->enabled(true)
+ ->persistenceEnabled(true)
+ // if persistence is enabled and symfony/cache is installed, null otherwise
+ ->persistenceAdapter('kreyu_data_table.sorting.persistence.adapter.cache')
+ // if persistence is enabled and symfony/security-bundle is installed, null otherwise
+ ->persistenceSubjectProvider('kreyu_data_table.persistence.subject_provider.token_storage')
+ ;
+
+ $defaults->pagination()
+ ->enabled(true)
+ ->persistenceEnabled(true)
+ // if persistence is enabled and symfony/cache is installed, null otherwise
+ ->persistenceAdapter('kreyu_data_table.pagination.persistence.adapter.cache')
+ // if persistence is enabled and symfony/security-bundle is installed, null otherwise
+ ->persistenceSubjectProvider('kreyu_data_table.persistence.subject_provider.token_storage')
+ ;
+
+ $defaults->filtration()
+ ->enabled(true)
+ ->persistenceEnabled(true)
+ // if persistence is enabled and symfony/cache is installed, null otherwise
+ ->persistenceAdapter('kreyu_data_table.filtration.persistence.adapter.cache')
+ // if persistence is enabled and symfony/security-bundle is installed, null otherwise
+ ->persistenceSubjectProvider('kreyu_data_table.persistence.subject_provider.token_storage')
+ ;
+
+ $defaults->personalization()
+ ->enabled(true)
+ ->persistenceEnabled(true)
+ // if persistence is enabled and symfony/cache is installed, null otherwise
+ ->persistenceAdapter('kreyu_data_table.personalization.persistence.adapter.cache')
+ // if persistence is enabled and symfony/security-bundle is installed, null otherwise
+ ->persistenceSubjectProvider('kreyu_data_table.persistence.subject_provider.token_storage')
+ ;
+
+ $defaults->exporting()
+ ->enabled(true)
+ ->formFactory('form.factory')
+ ->exporterFactory('kreyu_data_table.exporter.factory')
+ ;
+};
+```
+
+
+::: tip
+The default cache persistence adapters are provided only if the [Symfony Cache](https://symfony.com/doc/current/components/cache.html) component is installed.
+If the component is not installed, then the default value equals null, meaning you'll have to specify an adapter manually if you wish to use the persistence.
+:::
+
+::: tip
+The persistence subject providers are provided only if the [Symfony Security](https://symfony.com/doc/current/security.html) component is installed.
+If the component is not installed, then the default value equals null, meaning you'll have to specify a subject provider manually if you wish to use the persistence.
+:::
diff --git a/docs/src/reference/twig.md b/docs/src/reference/twig.md
new file mode 100644
index 00000000..d398972d
--- /dev/null
+++ b/docs/src/reference/twig.md
@@ -0,0 +1,218 @@
+# Twig
+
+## Functions
+
+Even though the helper functions simply renders a template block of a specific part of the data table,
+they are very useful because they take use the theme configured in bundle.
+
+### `data_table`
+
+**Usage**: `data_table(data_table_view, variables)`
+
+Renders the HTML of a complete data table, with action bar, filtration, pagination, etc.
+
+```twig
+{{ data_table(data_table, { 'title': 'Products' }) }}
+```
+
+You will mostly use this helper for prototyping or if you use custom theme.
+If you need more flexibility in rendering the data table, you should use the other helpers
+to render individual parts of the data table instead.
+
+### `data_table_table`
+
+**Usage**: `data_table_table(data_table_view, variables)`
+
+Renders the HTML of the data table.
+
+### `data_table_action_bar`
+
+**Usage**: `data_table_action_bar(data_table_view, variables)`
+
+Renders the HTML of the data table action bar, which includes filtration, exporting and personalization features.
+
+### `data_table_header_row`
+
+**Usage**: `data_table_header_row(header_row_view, variables)`
+
+Renders the header row of the data table.
+
+### `data_table_value_row`
+
+**Usage**: `data_table_value_row(value_row_view, variables)`
+
+Renders the value row of the data table.
+
+### `data_table_column_label`
+
+**Usage**: `data_table_column_label(column_view, variables)`
+
+Renders the label of the column. This takes care of all the label translation logic under the hood.
+
+### `data_table_column_header`
+
+**Usage**: `data_table_column_header(column_view, variables)`
+
+Renders the header of the column. Internally, this does the same as `data_table_column_label()` method,
+but additionally handles the sorting feature.
+
+### `data_table_column_value`
+
+**Usage**: `data_table_column_value(column_view, variables)`
+
+Renders the value of the column. It handles all the required logic to extract value from the row data
+based on the column configuration (e.g. to display formatted `name` of the `Project` entity).
+
+### `data_table_filters_form`
+
+**Usage**: `data_table_filters_form(form, variables)`
+
+Renders the filters form. Accepts both the `FormInterface` and `FormView`.
+If given value is instance of `FormInterface`, the `createView()` method will be called.
+
+### `data_table_personalization_form`
+
+**Usage**: `data_table_personalization_form(form, variables)`
+
+Renders the personalization form. Accepts both the `FormInterface` and `FormView`.
+If given value is instance of `FormInterface`, the `createView()` method will be called.
+
+### `data_table_export_form`
+
+**Usage**: `data_table_export_form(form, variables)`
+
+Renders the export form. Accepts both the `FormInterface` and `FormView`.
+If given value is instance of `FormInterface`, the `createView()` method will be called.
+
+### `data_table_pagination`
+
+**Usage**: `data_table_pagination(pagination_view, variables)`
+
+Renders the pagination controls.
+
+Additionally, accepts the data table view as a first argument.
+In this case, the pagination view is extracted from the data table view "pagination" variable.
+
+## Variables
+
+Certain types may define even more variables, and some variables here only really apply to certain types.
+To know the exact variables available for each type, check out the code of the templates used by your data table theme.
+
+::: warning The type classes are constantly changing before the stable release!
+Check source code of the type class to make sure of the variables exposed to the template.
+:::
+
+### Data table variables
+
+The following variables are common to every data table type:
+
+| Variable | Usage |
+|----------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|
+| `themes` | Themes to apply for the data table. |
+| `name` | Name of the data table. |
+| `title` | Title of the data table |
+| `title_translation_parameters` | Parameters used in title translation. |
+| `translation_domain` | Translation domain used in translatable strings in the data table. If `false`, the translation is disabled. |
+| `pagination_enabled` | If `true`, the pagination feature is enabled. |
+| `sorting_enabled` | If `true`, the sorting feature is enabled. |
+| `filtration_enabled` | If `true`, the filtration feature is enabled. |
+| `personalization_enabled` | If `true`, the personalization feature is enabled. |
+| `exporting_enabled` | If `true`, the exporting feature is enabled. |
+| `page_parameter_name` | Name of the parameter that holds the current page number. |
+| `per_page_parameter_name` | Name of the parameter that holds the pagination per page limit. |
+| `sort_parameter_name` | Name of the parameter that holds the sorting data array (e.g. `[{sort_parameter_name}][field]`, `[{sort_parameter_name}][direction]`). |
+| `filtration_parameter_name` | Name of the parameter that holds the filtration form data. |
+| `personalization_parameter_name` | Name of the parameter that holds the personalization form data. |
+| `export_parameter_name` | Name of the parameter that holds the export form data. |
+| `has_active_filters` | If at least one filter is active, this value will equal `true`. |
+| `filtration_data` | An instance of filtration data, that contains applied filters values. |
+| `sorting_data` | An instance of sorting data, that contains applied sorting values. |
+| `header_row` | An instance of headers row view. |
+| `non_personalized_header_row` | An instance of headers row view without personalization applied. |
+| `value_rows` | A list of instances of value rows views. |
+| `pagination` | An instance of pagination. |
+| `actions` | A list of actions defined for the data table. |
+| `filters` | A list of filters defined for the data table. |
+| `exporters` | A list of exporters defined for the data table. |
+| `column_count` | Holds count of the columns, respecting the personalization. |
+| `filtration_form` | Holds an instance of the filtration form view. |
+| `personalization_form` | Holds an instance of the personalization form view. |
+| `export_form` | Holds an instance of the export form view. |
+
+### Column header variables
+
+The following variables are common to every column type header:
+
+| Variable | Usage |
+|--------------------------|----------------------------------------------------------------------------------------------------------------------------------------|
+| `name` | Name of the column. |
+| `column` | An instance of column view. |
+| `row` | An instance of header row that the column belongs to. |
+| `data_table` | An instance of data table view. |
+| `label` | Label that will be used when rendering the column header. |
+| `translation_parameters` | Parameters used when translating the header translatable values (e.g. label). |
+| `translation_domain` | Translation domain used when translating the column translatable values. If `false`, the translation is disabled. |
+| `sort_parameter_name` | Name of the parameter that holds the sorting data array (e.g. `[{sort_parameter_name}][field]`, `[{sort_parameter_name}][direction]`). |
+| `sorted` | Determines whether the column is currently being sorted. |
+| `sort_field` | Sort field used by the sortable behavior. If `false`, the sorting is disabled for the column. |
+| `sort_direction` | Direction in which the column is currently being sorted. |
+| `block_prefixes` | A list of block prefixes respecting the type inheritance. |
+| `export` | An array of export options, including `label` and `translation_domain` options. Equals `false` if the column is not exportable. |
+| `attr` | An array of attributes used in rendering the column header. |
+
+### Column value variables
+
+The following variables are common to every column type value:
+
+| Variable | Usage |
+|--------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
+| `row` | An instance of value row that the column belongs to. |
+| `data_table` | An instance of data table view. |
+| `data` | Holds the norm data of a column. |
+| `value` | Holds the string representation of a column value. |
+| `translation_parameters` | Parameters used when translating the translatable values. |
+| `translation_domain` | Translation domain used when translating the column translatable values. If `false`, the translation is disabled. |
+| `block_prefixes` | A list of block prefixes respecting the type inheritance. |
+| `export` | An array of export options, including `data`, `value`, `label` and `translation_domain` options. Equals `false` if the column is not exportable. |
+| `attr` | An array of attributes used in rendering the column value. |
+
+### Filter variables
+
+The following variables are common to every filter type:
+
+| Variable | Usage |
+|--------------------------------|-------------------------------------------------------------------------------------------------------------------|
+| `name` | Name of the filter. |
+| `form_name` | Form field name of the column. |
+| `label` | Label that will be used when rendering the column header. |
+| `label_translation_parameters` | Parameters used when translating the `label` option. |
+| `translation_domain` | Translation domain used when translating the column translatable values. If `false`, the translation is disabled. |
+| `query_path` | Field name used in the query (e.g. in DQL, like `product.name`) |
+| `field_type` | FQCN of the form field type used to render the filter control. |
+| `field_options` | Array of options passed to the form type defined in the `field_type`. |
+| `operator_type` | FQCN of the form field type used to render the operator control. |
+| `operator_options` | Array of options passed to the form type defined in the `operator_type`. |
+| `data` | Holds the norm data of a filter. |
+| `value` | Holds the string representation of a filter value. |
+
+### Action variables
+
+The following variables are common to every action type:
+
+| Variable | Usage |
+|--------------------------|-------------------------------------------------------------------------------------------------------------------|
+| `name` | Name of the action. |
+| `label` | Name of the action. |
+| `data_table` | An instance of data table view. |
+| `block_prefixes` | A list of block prefixes respecting the type inheritance. |
+| `translation_domain` | Translation domain used when translating the action translatable values. If `false`, the translation is disabled. |
+| `translation_parameters` | Parameters used when translating the action translatable values (e.g. label). |
+| `attr` | An array of attributes used in rendering the action. |
+| `icon_attr` | An array of attributes used in rendering the action icon. |
+| `confirmation` | An array of action confirmation options. If `false`, action is not confirmable. |
+
+::: tip
+Behind the scenes, these variables are made available to the `DataTableView`, `ColumnView` and `FilterView` objects of your data table
+when the DataTable component calls `buildView()`. To see what "view" variables a particular type has,
+find the source code for the used type class and look for the `buildView()` method.
+:::
diff --git a/docs/src/reference/types/action.md b/docs/src/reference/types/action.md
new file mode 100644
index 00000000..8573bfe8
--- /dev/null
+++ b/docs/src/reference/types/action.md
@@ -0,0 +1,8 @@
+# Action types
+
+The following action types are natively available in the bundle:
+
+- [Link](#)
+- [Button](#)
+- [Callback](#)
+- [Action](#)
diff --git a/docs/src/reference/types/action/action.md b/docs/src/reference/types/action/action.md
new file mode 100644
index 00000000..2ab71a04
--- /dev/null
+++ b/docs/src/reference/types/action/action.md
@@ -0,0 +1,11 @@
+
+
+# ActionType
+
+The [`ActionType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/ActionType.php) represents a base action, used as a parent for every other action type in the bundle.
+
+## Options
+
+
diff --git a/docs/src/reference/types/action/button.md b/docs/src/reference/types/action/button.md
new file mode 100644
index 00000000..84111f51
--- /dev/null
+++ b/docs/src/reference/types/action/button.md
@@ -0,0 +1,47 @@
+
+
+# ButtonActionType
+
+The [`ButtonActionType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/ButtonActionType.php) represents an action rendered as a button.
+
+## Options
+
+### `href`
+
+- **type**: `string` or `callable`
+- **default**: `'#'`
+
+A value used as an action link [href attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-href).
+
+```php #
+use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType;
+
+$builder
+ ->addAction('back', ButtonActionType::class, [
+ 'href' => $this->urlGenerator->generate('category_index'),
+ ])
+;
+```
+
+### `target`
+
+- **type**: `string` or `callable`
+- **default**: `'_self'`
+
+Sets the value that will be used as an anchor [target attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target).
+
+```php #
+use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType;
+
+$builder
+ ->addAction('wiki', ButtonActionType::class, [
+ 'target' => '_blank',
+ ])
+;
+```
+
+## Inherited options
+
+
diff --git a/docs/src/reference/types/action/form.md b/docs/src/reference/types/action/form.md
new file mode 100644
index 00000000..6409d693
--- /dev/null
+++ b/docs/src/reference/types/action/form.md
@@ -0,0 +1,66 @@
+
+
+# FormActionType
+
+The [`FormActionType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/FormActionType.php) represents an action rendered as a submit button to a hidden form, which allows the action to use any HTTP method.
+
+## Options
+
+### `action`
+
+- **type**: `string` or `callable`
+- **default**: `'#'`
+
+Sets the value that will be used as a form's `action` attribute.
+
+```php
+use Kreyu\Bundle\DataTableBundle\Action\Type\FormActionType;
+
+$builder
+ ->addAction('send', FormActionType::class, [
+ 'action' => $this->urlGenerator->generate('sms_send'),
+ ])
+;
+```
+
+### `method`
+
+- **type**: `string` or `callable`
+- **default**: `'GET'`
+
+Sets the value that will be used as a form's `method` attribute.
+
+```php
+use Kreyu\Bundle\DataTableBundle\Action\Type\FormActionType;
+
+$builder
+ ->addAction('send', FormActionType::class, [
+ 'method' => 'POST',
+ ])
+;
+```
+
+### `button_attr`
+
+- **type**: `array`
+- **default**: `[]`
+
+An array of attributes used to render the form submit button.
+
+```php
+use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType;
+
+$builder
+ ->addAction('remove', ButtonActionType::class, [
+ 'attr' => [
+ 'class' => 'btn btn-danger',
+ ],
+ ])
+;
+```
+
+## Inherited options
+
+
diff --git a/docs/src/reference/types/action/link.md b/docs/src/reference/types/action/link.md
new file mode 100644
index 00000000..7b158961
--- /dev/null
+++ b/docs/src/reference/types/action/link.md
@@ -0,0 +1,47 @@
+
+
+# LinkActionType
+
+The [`LinkActionType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/LinkActionType.php) represents an action rendered as a simple link.
+
+## Options
+
+### `href`
+
+- **type**: `string` or `callable`
+- **default**: `'#'`
+
+A value used as an action link [href attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-href).
+
+```php #
+use Kreyu\Bundle\DataTableBundle\Action\Type\LinkActionType;
+
+$builder
+ ->addAction('back', LinkActionType::class, [
+ 'href' => $this->urlGenerator->generate('category_index'),
+ ])
+;
+```
+
+### `target`
+
+- **type**: `string` or `callable`
+- **default**: `'_self'`
+
+Sets the value that will be used as an anchor [target attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target).
+
+```php #
+use Kreyu\Bundle\DataTableBundle\Action\Type\LinkActionType;
+
+$builder
+ ->addAction('wiki', LinkActionType::class, [
+ 'target' => '_blank',
+ ])
+;
+```
+
+## Inherited options
+
+
diff --git a/docs/src/reference/types/action/options/action.md b/docs/src/reference/types/action/options/action.md
new file mode 100644
index 00000000..21ac83c2
--- /dev/null
+++ b/docs/src/reference/types/action/options/action.md
@@ -0,0 +1,135 @@
+### `label`
+
+- **type**: `null`, `string` or `Symfony\Component\Translation\TranslatableInterface`
+- **default**: `null`
+
+A label representing the action.
+When value equals `null`, a sentence cased action name is used as a label, for example:
+
+| Action name | Guessed label |
+|--------------|----------------|
+| create | Create |
+| saveAndClose | Save and close |
+
+### `label_translation_parameters`
+
+- **type**: `array`
+- **default**: `[]`
+
+An array of parameters used to translate the action label.
+
+### `translation_domain`
+
+- **type**: `false` or `string`
+- **default**: `'KreyuDataTable'`
+
+Translation domain used in translation of action's translatable values.
+
+### `block_prefix`
+
+- **type**: `string`
+- **default**: value returned by the action type `getBlockPrefix()` method
+
+Allows you to add a custom block prefix and override the block name used to render the action type.
+Useful, for example, if you have multiple instances of the same action type, and you need to personalize
+the rendering of some of them, without the need to create a new action type.
+
+### `visible`
+
+- **type**: `bool` or `callable`
+- **default**: `true`
+
+Determines whether the action should be visible to the user.
+
+The callable can only be used by the row actions to determine visibility [based on the row data](../../../../docs/components/actions.md#using-row-data-in-options):
+
+```php
+use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType;
+
+$builder
+ ->addRowAction('remove', ButtonActionType::class, [
+ 'visible' => function (Product $product) {
+ return $product->isRemovable();
+ },
+ ])
+;
+```
+
+### `attr`
+
+- **type**: `array`
+- **default**: `[]`
+
+An array of attributes used to render the action.
+
+```php
+use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType;
+
+$builder
+ ->addAction('remove', ButtonActionType::class, [
+ 'attr' => [
+ 'class' => 'bg-danger',
+ ],
+ ])
+;
+```
+
+### `icon_attr`
+
+- **type**: `array`
+- **default**: `[]`
+
+An array of attributes used to render the action's icon.
+
+```php
+use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType;
+
+$builder
+ ->addAction('remove', ButtonActionType::class, [
+ 'icon_attr' => [
+ 'class' => 'ti ti-trash',
+ ],
+ ])
+;
+```
+
+### `confirmation`
+
+- **type**: `bool`, `array` or `callable`
+- **default**: `false`
+
+Determines whether the action is confirmable, which displays a modal where user have to acknowledge the process.
+The modal can be configured by passing an array with the following options:
+
+> #### `translation_domain`
+>
+> - **type**: `false` or `string`
+> - **default**: `'KreyuDataTable'`
+>
+> #### `label_title`
+>
+> - **type**: `null` or `string`
+> - **default**: `'Action confirmation'`
+>
+> #### `label_description`
+>
+> - **type**: `null` or `string`
+> - **default**: `'Are you sure you want to execute this action?'`
+>
+> #### `label_confirm`
+>
+> - **type**: `null` or `string`
+> - **default**: `'Confirm'`
+>
+> #### `label_cancel`
+>
+> - **type**: `null` or `string`
+> - **default**: `'Cancel'`
+>
+> #### `type`
+>
+> - **type**: `null` or `string`
+> - **default**: `danger`
+> - **allowed values**: `danger`, `warning`, `info`
+>
+> Represents a type of the action confirmation, which determines the color of the displayed modal.
diff --git a/docs/src/reference/types/column.md b/docs/src/reference/types/column.md
new file mode 100644
index 00000000..40463fb6
--- /dev/null
+++ b/docs/src/reference/types/column.md
@@ -0,0 +1,20 @@
+# Column types
+
+The following column types are natively available in the bundle:
+
+- Text columns
+ - [Text](column/text.md)
+ - [Number](column/number.md)
+ - [Money](column/money.md)
+ - [Boolean](column/boolean.md)
+ - [Link](column/link.md)
+- Date and time columns
+ - [DateTime](column/date-time.md)
+ - [DatePeriod](column/date-period.md)
+- Special columns
+ - [Collection](column/collection.md)
+ - [Template](column/template.md)
+ - [Actions](column/actions.md)
+ - [Checkbox](column/checkbox.md)
+- Base columns
+ - [Column](column/column.md)
\ No newline at end of file
diff --git a/docs/src/reference/types/column/actions.md b/docs/src/reference/types/column/actions.md
new file mode 100644
index 00000000..4d30a59e
--- /dev/null
+++ b/docs/src/reference/types/column/actions.md
@@ -0,0 +1,74 @@
+
+
+# ActionsColumnType
+
+The [`ActionsColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/ActionsColumnType.php) represents a column that contains row actions.
+
+::: info In most cases, it is not necessary to use this column type directly.
+Instead, use data table builder's `addRowAction()` method.
+If at least one row action is defined and is visible, an `ActionColumnType` is added to the data table.
+:::
+
+## Options
+
+### `actions`
+
+- **type**: `array`
+- **default**: `[]`
+
+This option contains a list of actions. Each action consists of _three_ options:
+
+> #### `type`
+>
+> - **type**: `string`
+>
+> Fully qualified class name of the [action type](#).
+>
+>
+> #### `visible`
+>
+> - **type**: `bool`or `callable`
+> - **default**: `true`
+
+Determines whether the action should be visible.
+
+Example usage:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType;
+use Kreyu\Bundle\DataTableBundle\Column\Type\ActionsColumnType;
+
+$builder
+ ->addColumn('actions', ActionsColumnType::class, [
+ 'actions' => [
+ 'show' => [
+ 'type' => ButtonActionType::class,
+ 'type_options' => [
+ 'url' => function (Product $product): string {
+ return $this->urlGenerator->generate('category_show', [
+ 'id' => $product->getId(),
+ ]);
+ }),
+ ],
+ 'visible' => function (Product $product): bool {
+ return $product->isActive();
+ }
+ ],
+ ],
+ ])
+;
+```
+
+## Inherited options
+
+
diff --git a/docs/src/reference/types/column/boolean.md b/docs/src/reference/types/column/boolean.md
new file mode 100644
index 00000000..40e29aa8
--- /dev/null
+++ b/docs/src/reference/types/column/boolean.md
@@ -0,0 +1,27 @@
+
+
+# BooleanColumnType
+
+The [`BooleanColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/NumberColumnType.php) represents a column with value displayed as a "yes" or "no".
+
+## Options
+
+### `label_true`
+
+- **type**: `string` or `Symfony\Component\Translation\TranslatableInterface`
+- **default**: `'Yes'`
+
+Sets the value that will be displayed if value is truthy.
+
+### `label_false`
+
+- **type**: `string` or `Symfony\Component\Translation\TranslatableInterface`
+- **default**: `'No'`
+
+Sets the value that will be displayed if row value is falsy.
+
+## Inherited options
+
+
diff --git a/docs/src/reference/types/column/checkbox.md b/docs/src/reference/types/column/checkbox.md
new file mode 100644
index 00000000..46dff6c6
--- /dev/null
+++ b/docs/src/reference/types/column/checkbox.md
@@ -0,0 +1,32 @@
+
+
+# CheckboxColumnType
+
+The [`CheckboxColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/CheckboxColumnType.php) represents a column with checkboxes, both in header and value rows.
+
+::: info In most cases, it is not necessary to use this column type directly.
+Instead, use data table builder's `addBatchAction()` method.
+If at least one batch action is defined and is visible, an `BatchActionType` is added to the data table.
+
+For details, see [adding checkbox column](../../../docs/components/actions.md#adding-checkbox-column) section of the action documentation.
+:::
+
+## Options
+
+### `identifier_name`
+
+- **type**: `string`
+- **default**: `'id'`
+
+A name of the property to use in the batch actions.
+
+For more details about this option's influence on the batch actions, see ["Changing the identifier parameter name"](#) section.
+
+## Inherited options
+
+
diff --git a/docs/src/reference/types/column/collection.md b/docs/src/reference/types/column/collection.md
new file mode 100644
index 00000000..490f5abb
--- /dev/null
+++ b/docs/src/reference/types/column/collection.md
@@ -0,0 +1,62 @@
+
+
+# CollectionColumnType
+
+The [`CollectionColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/CollectionColumnType.php) represents a column with value displayed as a list.
+
+## Options
+
+### `entry_type`
+
+- **type**: `string`
+- **default**: `'Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType'`
+
+This is the column type for each item in this collection (e.g. [TextColumnType](text.md), [LinkColumnType](link.md), etc).
+For example, if you have an array of entities, you'd probably want to use the [LinkColumnType](link.md) to display them as links to their details view.
+
+### `entry_options`
+
+- **type**: `array`
+- **default**: `['property_path' => false]`
+
+This is the array that's passed to the column type specified in the entry_type option.
+For example, if you used the [LinkColumnType](link.md) as your `entry_type` option (e.g. for a collection of links of product tags),
+then you'd want to pass the href option to the underlying type:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Column\Type\CollectionColumnType;
+use Kreyu\Bundle\DataTableBundle\Column\Type\LinkColumnType;
+
+$builder
+ ->addColumn('tags', CollectionColumnType::class, [
+ 'entry_type' => LinkColumnType::class,
+ 'entry_options' => [
+ 'href' => function (Tag $tag): string {
+ return $this->urlGenerator->generate('tag_show', [
+ 'id' => $tag->getId(),
+ ]);
+ },
+ 'formatter' => function (Tag $tag): string {
+ return $tag->getName();
+ },
+ ],
+ ])
+;
+```
+
+!!! Note
+The options resolver normalizer ensures the `property_path` is always present in the `entry_options` array, and it defaults to `false`.
+!!!
+
+### `separator`
+
+- **type**: `null` or `string`
+- **default**: `', '`
+
+Sets the value displayed between every item in the collection.
+
+## Inherited options
+
+
diff --git a/docs/src/reference/types/column/column.md b/docs/src/reference/types/column/column.md
new file mode 100644
index 00000000..2992eedc
--- /dev/null
+++ b/docs/src/reference/types/column/column.md
@@ -0,0 +1,11 @@
+
+
+# ColumnType
+
+The [`ColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/ColumnType.php) represents a base column, used as a parent for every other column type in the bundle.
+
+## Options
+
+
diff --git a/docs/src/reference/types/column/date-period.md b/docs/src/reference/types/column/date-period.md
new file mode 100644
index 00000000..646e1c97
--- /dev/null
+++ b/docs/src/reference/types/column/date-period.md
@@ -0,0 +1,34 @@
+
+
+# DatePeriodColumnType
+
+The [`DatePeriodColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/DatePeriodColumnType.php) represents a column with value displayed as a date (and with time by default).
+
+## Options
+
+### `format`
+
+- **type**: `string`
+- **default**: `'d.m.Y H:i:s'`
+
+The format specifier is the same as supported by [date](https://www.php.net/date).
+
+### `timezone`
+
+- **type**: `null` or `string`
+- **default**: `null`
+
+A timezone used to render the dates as string.
+
+### `separator`
+
+- **type**: `string`
+- **default**: `' - '`
+
+Separator to display between the dates.
+
+## Inherited options
+
+
diff --git a/docs/src/reference/types/column/date-time.md b/docs/src/reference/types/column/date-time.md
new file mode 100644
index 00000000..70439492
--- /dev/null
+++ b/docs/src/reference/types/column/date-time.md
@@ -0,0 +1,27 @@
+
+
+# DateTimeColumnType
+
+The [`DateTimeColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/DateTimeColumnType.php) represents a column with value displayed as a date and time.
+
+## Options
+
+### `format`
+
+- **type**: `string`
+- **default**: `'d.m.Y H:i:s'`
+
+The format specifier is the same as supported by [date](https://www.php.net/date).
+
+### `timezone`
+
+- **type**: `null` or `string`
+- **default**: `null`
+
+A timezone used to render the date time as string.
+
+## Inherited options
+
+
diff --git a/docs/src/reference/types/column/date.md b/docs/src/reference/types/column/date.md
new file mode 100644
index 00000000..25257a9c
--- /dev/null
+++ b/docs/src/reference/types/column/date.md
@@ -0,0 +1,29 @@
+
+
+# DateColumnType
+
+The [`DateColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/DateColumnType.php) represents a column with value displayed as a date.
+
+This column type works exactly like `DateTimeColumnType`, but has a different default format.
+
+## Options
+
+### `format`
+
+- **type**: `string`
+- **default**: `'d.m.Y'`
+
+The format specifier is the same as supported by [date](https://www.php.net/date).
+
+### `timezone`
+
+- **type**: `null` or `string`
+- **default**: `null`
+
+A timezone used to render the date time as string.
+
+## Inherited options
+
+
diff --git a/docs/src/reference/types/column/link.md b/docs/src/reference/types/column/link.md
new file mode 100644
index 00000000..6462e2ea
--- /dev/null
+++ b/docs/src/reference/types/column/link.md
@@ -0,0 +1,42 @@
+
+
+# LinkColumnType
+
+The [`LinkColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/LinkColumnType.php) represents a column with value displayed as a link.
+
+## Options
+
+### `href`
+
+- **type**: `string` or `callable`
+- **default**: `'#'`
+
+Sets the value that will be used as a [href attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-href).
+
+```php
+use App\Entity\Category;
+use Kreyu\Bundle\DataTableBundle\Column\Type\LinkColumnType;
+
+$builder
+ ->addColumn('category', LinkColumnType::class, [
+ 'href' => function (Category $category): string {
+ return $this->urlGenerator->generate('category_show', [
+ 'id' => $category->getId(),
+ ]);
+ },
+ ])
+;
+```
+
+### `target`
+
+- **type**: `string` or `callable`
+- **default**: `'_self'`
+
+Sets the value that will be used as a [target attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target).
+
+## Inherited options
+
+
diff --git a/docs/src/reference/types/column/money.md b/docs/src/reference/types/column/money.md
new file mode 100644
index 00000000..3c254c23
--- /dev/null
+++ b/docs/src/reference/types/column/money.md
@@ -0,0 +1,81 @@
+
+
+# MoneyColumnType
+
+The [`MoneyColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/MoneyColumnType.php) represents a column with monetary value, appropriately formatted and rendered with currency sign.
+
+## Options
+
+### `currency`
+
+- **type**: `string` or `callable` - any [3-letter ISO 4217 code](https://en.wikipedia.org/wiki/ISO_4217)
+
+Specifies the currency that the money is being specified in.
+This determines the currency symbol that should be shown in the column.
+
+When using the [Intl number formatter](https://www.php.net/manual/en/class.numberformatter.php),
+the ISO code will be automatically converted to the appropriate currency sign, for example:
+
+- `EUR` becomes `€`;
+- `PLN` becomes `zł`;
+
+Please note that the end result is also dependent on the locale used in the application, for example, with value of `1000`:
+
+- `USD` currency will be rendered as `$1,000.00` when using the `en` locale;
+- `USD` currency will be rendered as `1 000,00 USD` when using the `pl` locale;
+
+When the Intl formatter is **NOT** used, given currency is simply rendered after the raw value, e.g. `1000 USD`.
+
+Additionally, the option accepts a callable, which gets a row data as first argument:
+
+```php
+$builder
+ ->addColumn('price', MoneyColumnType::class, [
+ 'currency' => fn (Product $product) => $product->getPriceCurrency(),
+ ])
+;
+```
+
+### `use_intl_formatter`
+
+- **type**: `bool`
+- **default**: `true` if [`symfony/intl`](https://packagist.org/packages/symfony/intl) is installed, `false` instead
+
+Determines whether the [Intl number formatter](https://www.php.net/manual/en/class.numberformatter.php) should be used.
+Enabling this option will automatically handle the formatting based on the locale set in the application.
+For example, value `123456.78` will be rendered differently:
+
+- `123,456.78` when using `en` locale;
+- `123 456,78` when using `pl` locale;
+- etc.
+
+### `intl_formatter_options`
+
+- **type**: `array`
+- **default**: `['attrs' => [], 'style' => 'decimal']`
+
+Configures the [Intl number formatter](https://www.php.net/manual/en/class.numberformatter.php) if used.
+For example, to limit decimal places to two:
+
+```php
+$builder
+ ->addColumn('price', MoneyColumnType::class, [
+ 'intl_formatter_options' => [
+ 'attrs' => [
+ // https://www.php.net/manual/en/class.numberformatter.php#numberformatter.constants.max-fraction-digits
+ 'max_fraction_digit' => 2,
+ ],
+ ])
+ ])
+;
+```
+
+For more details, see:
+- [Intl number formatter documentation](https://www.php.net/manual/en/class.numberformatter.php)
+- [Twig `format_currency` filter documentation](https://twig.symfony.com/doc/2.x/filters/format_currency.html)
+
+## Inherited options
+
+
diff --git a/docs/src/reference/types/column/number.md b/docs/src/reference/types/column/number.md
new file mode 100644
index 00000000..5c50a04e
--- /dev/null
+++ b/docs/src/reference/types/column/number.md
@@ -0,0 +1,51 @@
+
+
+# NumberColumnType
+
+The [`NumberColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/NumberColumnType.php) represents a column with value displayed as a number.
+
+## Options
+
+### `use_intl_formatter`
+
+- **type**: `bool`
+- **default**: `true` if [`symfony/intl`](https://packagist.org/packages/symfony/intl), is installed `false` instead
+
+Determines whether the [Intl number formatter](https://www.php.net/manual/en/class.numberformatter.php) should be used.
+Enabling this option will automatically handle the formatting based on the locale set in the application.
+For example, value `123456.78` will be rendered differently:
+
+- `123,456.78` when using `en` locale;
+- `123 456,78` when using `pl` locale;
+- etc.
+
+### `intl_formatter_options`
+
+- **type**: `array`
+- **default**: `['attrs' => [], 'style' => 'decimal']`
+
+Configures the [Intl number formatter](https://www.php.net/manual/en/class.numberformatter.php) if used.
+For example, to limit decimal places to two:
+
+```php
+$builder
+ ->addColumn('price', NumberColumnType::class, [
+ 'intl_formatter_options' => [
+ 'attrs' => [
+ // https://www.php.net/manual/en/class.numberformatter.php#numberformatter.constants.max-fraction-digits
+ 'max_fraction_digit' => 2,
+ ],
+ ])
+ ])
+;
+```
+
+For more details, see:
+- [Intl number formatter documentation](https://www.php.net/manual/en/class.numberformatter.php)
+- [Twig `format_number` filter documentation](https://twig.symfony.com/doc/2.x/filters/format_number.html)
+
+## Inherited options
+
+
diff --git a/docs/src/reference/types/column/options/column.md b/docs/src/reference/types/column/options/column.md
new file mode 100644
index 00000000..a067cbb6
--- /dev/null
+++ b/docs/src/reference/types/column/options/column.md
@@ -0,0 +1,226 @@
+
+
+### `label`
+
+- **type**: `null`, `string` or `Symfony\Component\Translation\TranslatableInterface`
+- **default**: `{{ defaults.label }}`
+
+Sets the label that will be used in column header and personalization column list.
+
+When value equals `null`, a sentence cased column name is used as a label, for example:
+
+| Column name | Guessed label |
+|-------------|---------------|
+| name | Name |
+| firstName | First name |
+
+### `header_translation_domain`
+
+- **type**: `false` or `string`
+- **default**: `'KreyuDataTable'`
+
+Sets the translation domain used when translating the column header.
+Setting the option to `false` disables its translation.
+
+### `header_translation_parameters`
+
+- **type**: `array`
+- **default**: `[]`
+
+Sets the parameters used when translating the column header.
+
+### `value_translation_domain`
+
+- **type**: `false` or `string`
+- **default**: inherited from the data table translation domain
+
+Sets the translation domain used when translating the column value.
+Setting the option to `false` disables its translation.
+
+### `property_path`
+
+- **type**: `null`, `false` or `string`
+- **default**: `{{ defaults.property_path }}`
+
+Sets the property path used by the [PropertyAccessor](https://symfony.com/doc/current/components/property_access.html) to retrieve column value of each row.
+Setting the option to `false` disables property accessor.
+
+```php
+$builder
+ ->addColumn('category', TextColumnType::class, [
+ 'property_path' => 'category.name',
+ ])
+;
+```
+
+When value equals `null`, the column name is used as a property path.
+
+### `getter`
+
+- **type**: `null` or `callable`
+- **default**: `null`
+
+When provided, this callable will be invoked to read the value from the underlying object that will be used within the column.
+This disables the usage of the [PropertyAccessor](https://symfony.com/doc/current/components/property_access.html), described in the [property_path](#property_path) option.
+
+```php
+$builder
+ ->addColumn('category', TextColumnType::class, [
+ 'getter' => fn (Product $product) => $product->getCategory(),
+ ])
+;
+```
+
+### `sort`
+
+- **type**: `bool` or `string`
+- **default**: `false` - the sortable behavior is disabled
+
+Sets the sort field used by the sortable behavior. Setting the option to:
+- `true` - enables column sorting and uses the column name as a sort field name;
+- `false` - disables column sorting;
+- string - defines sort property path;
+
+### `block_prefix`
+
+- **type**: `string`
+- **default**: value returned by the column type `getBlockPrefix()` method
+
+Allows you to add a custom block prefix and override the block name used to render the column type.
+Useful for example if you have multiple instances of the same column type, and you need to personalize
+the rendering of some of them, without the need to create a new column type.
+
+### `formatter`
+
+- **type**: `null` or `callable`
+- **default**: `null`
+
+Formats the value to the desired string.
+
+```php
+$builder
+ ->addColumn('quantity', NumberColumnType::class, [
+ 'formatter' => fn (float $value) => number_format($value, 2) . 'kg',
+ ])
+;
+```
+
+### `export`
+
+- **type**: `bool` or `array`
+- **default**: `[]`
+
+This option accepts an array of options available for the column type.
+It is used to differentiate options for regular rendering, and excel rendering.
+
+For example, if you wish to display quantity column with "Quantity" label, but export with a "Qty" header:
+
+```php
+$builder
+ ->addColumn('quantity', NumberColumnType::class, [
+ 'label' => 'Quantity',
+ 'translation_domain' => 'product',
+ 'export' => [
+ 'label' => 'Qty',
+ // rest of the options are inherited, therefore "translation_domain" equals "product", etc.
+ ],
+ ])
+;
+```
+
+Rest of the options are inherited from the column options.
+
+Setting this option to `true` automatically copies the column options as the export column options.
+Setting this option to `false` excludes the column from the exports.
+
+### `header_attr`
+
+- **type**: `array`
+- **default**: `[]`
+
+If you want to add extra attributes to an HTML column header representation (`
`) you can use the attr option.
+It's an associative array with HTML attributes as keys.
+This can be useful when you need to set a custom class for a column:
+
+```php
+$builder
+ ->addColumn('quantity', NumberColumnType::class, [
+ 'header_attr' => [
+ 'class' => 'text-end',
+ ],
+ ])
+;
+```
+
+### `value_attr`
+
+- **type**: `array` or `callable`
+- **default**: `[]`
+
+If you want to add extra attributes to an HTML column value representation (`
`) you can use the attr option.
+It's an associative array with HTML attributes as keys.
+This can be useful when you need to set a custom class for a column:
+
+```php
+$builder
+ ->addColumn('quantity', NumberColumnType::class, [
+ 'value_attr' => [
+ 'class' => 'text-end',
+ ],
+ ])
+;
+```
+
+You can pass a `callable` to perform a dynamic attribute generation:
+
+```php
+$builder
+ ->addColumn('quantity', NumberColumnType::class, [
+ 'value_attr' => function (int $quantity, Product $product) {
+ return [
+ 'class' => $quantity === 0 && !$product->isDisabled() ? 'text-danger' : '',
+ ],
+ },
+ ])
+;
+```
+
+### `priority`
+
+- **type**: `integer`
+- **default**: `0`
+
+Columns are rendered in the same order as they are included in the data table.
+This option changes the column rendering priority, allowing you to display columns earlier or later than their original order.
+
+The higher this priority, the earlier the column will be rendered.
+Priority can albo be negative and columns with the same priority will keep their original order.
+
+**Note**: column priority can be changed by the [personalization feature](../../../../docs/features/personalization.md).
+
+### `visible`
+
+- **type**: `bool`
+- **default**: `true`
+
+Determines whether the column is visible to the user.
+
+**Note**: column visibility can be changed by the [personalization feature](../../../../docs/features/personalization.md).
+
+### `personalizable`
+
+- **type**: `bool`
+- **default**: `true`
+
+Determines whether the column is personalizable.
+The non-personalizable columns are not modifiable by the [personalization feature](../../../../docs/features/personalization.md).
diff --git a/docs/src/reference/types/column/template.md b/docs/src/reference/types/column/template.md
new file mode 100644
index 00000000..82c8b705
--- /dev/null
+++ b/docs/src/reference/types/column/template.md
@@ -0,0 +1,25 @@
+
+
+# TemplateColumnType
+
+The [`TemplateColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/TemplateColumnType.php) represents a column with value displayed as a Twig template.
+
+## Options
+
+### `template_path`
+
+- **type**: `string` or `callable`
+
+Sets the path to the template that should be rendered.
+
+### `template_vars`
+
+- **type**: `string` or `callable`
+
+Sets the variables used within the template.
+
+## Inherited options
+
+
diff --git a/docs/src/reference/types/column/text.md b/docs/src/reference/types/column/text.md
new file mode 100644
index 00000000..195a21ce
--- /dev/null
+++ b/docs/src/reference/types/column/text.md
@@ -0,0 +1,15 @@
+
+
+# TextColumnType
+
+The [`TextColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/TextColumnType.php) represents a column with value displayed as a text.
+
+## Options
+
+This column type has no additional options.
+
+## Inherited options
+
+
diff --git a/docs/src/reference/types/data-table.md b/docs/src/reference/types/data-table.md
new file mode 100644
index 00000000..82da5634
--- /dev/null
+++ b/docs/src/reference/types/data-table.md
@@ -0,0 +1,150 @@
+# DataTable type
+
+The [`DataTableType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/ColumnType.php) represents a base data table, and should be used as a base for every data table defined in the system.
+
+## Options
+
+### `title`
+
+- **type**: `null`, `string` or `TranslatableInterface`
+- **default**: `null`
+
+### `title_translation_parameters`
+
+- **type**: `array`
+- **default**: `[]`
+
+### `translation_domain`
+
+- **type**: `null`, `bool` or `string`
+- **default**: `null`
+
+### `themes`
+
+- **type**: `string[]`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `column_factory`
+
+- **type**: `ColumnFactoryInterface`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `filter_factory`
+
+- **type**: `FilterFactoryInterface`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `action_factory`
+
+- **type**: `ActionFactoryInterface`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `exporter_factory`
+
+- **type**: `ExporterFactoryInterface`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `request_handler`
+
+- **type**: `RequestHandlerInterface`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `sorting_enabled`
+
+- **type**: `bool`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `sorting_persistence_enabled`
+
+- **type**: `bool`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `sorting_persistence_adapter`
+
+- **type**: `null` or `PersistenceAdapterInterface`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `sorting_persistence_subject_provider`
+
+- **type**: `null` or `PersistenceSubjectProviderInterface`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `pagination_enabled`
+
+- **type**: `bool`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `pagination_persistence_enabled`
+
+- **type**: `bool`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `pagination_persistence_adapter`
+
+- **type**: `null` or `PersistenceAdapterInterface`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `pagination_persistence_subject_provider`
+
+- **type**: `null` or `PersistenceSubjectProviderInterface`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `filtration_enabled`
+
+- **type**: `bool`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `filtration_persistence_enabled`
+
+- **type**: `bool`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `filtration_persistence_adapter`
+
+- **type**: `null` or `PersistenceAdapterInterface`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `filtration_persistence_subject_provider`
+
+- **type**: `null` or `PersistenceSubjectProviderInterface`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `filtration_form_factory`
+
+- **type**: `null` or `FormFactoryInterface`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `personalization_enabled`
+
+- **type**: `bool`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `personalization_persistence_enabled`
+
+- **type**: `bool`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `personalization_persistence_adapter`
+
+- **type**: `null` or `PersistenceAdapterInterface`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `personalization_persistence_subject_provider`
+
+- **type**: `null` or `PersistenceSubjectProviderInterface`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `personalization_form_factory`
+
+- **type**: `null` or `FormFactoryInterface`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `exporting_enabled`
+
+- **type**: `bool`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
+
+### `exporting_form_factory`
+
+- **type**: `null` or `FormFactoryInterface`
+- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults)
diff --git a/docs/src/reference/types/exporter.md b/docs/src/reference/types/exporter.md
new file mode 100644
index 00000000..accff261
--- /dev/null
+++ b/docs/src/reference/types/exporter.md
@@ -0,0 +1,25 @@
+# Exporter types
+
+The following action types are natively available in the bundle:
+
+- [Callback](#)
+- [Exporter](#)
+
+## PhpSpreadsheet
+
+The following [PhpSpreadsheet](https://github.com/PHPOffice/PhpSpreadsheet) exporter types are natively available in the bundle:
+
+- [Csv](./exporter/php-spreadsheet/csv)
+- [Xls](./exporter/php-spreadsheet/xls)
+- [Xlsx](./exporter/php-spreadsheet/xlsx)
+- [Ods](./exporter/php-spreadsheet/ods)
+- [Pdf](./exporter/php-spreadsheet/pdf)
+- [Html](./exporter/php-spreadsheet/html)
+
+## OpenSpout
+
+The following [OpenSpout](https://github.com/openspout/openspout) exporter types are natively available in the bundle:
+
+- [Csv](./exporter/open-spout/csv)
+- [Xlsx](./exporter/open-spout/xlsx)
+- [Ods](./exporter/open-spout/ods)
diff --git a/docs/src/reference/types/exporter/callback.md b/docs/src/reference/types/exporter/callback.md
new file mode 100644
index 00000000..3f7e3f7d
--- /dev/null
+++ b/docs/src/reference/types/exporter/callback.md
@@ -0,0 +1,34 @@
+
+
+# CallbackExporterType
+
+The [`CallbackExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/CallbackExporterType.php) represents a filter that uses a given callback as its handler.
+
+## Options
+
+### `callback`
+
+- **type**: `callable`
+
+Sets callable that works as an exporter handler.
+
+```php
+use Kreyu\Bundle\DataTableBundle\Exporter\Type\CallbackExporterType;
+use Kreyu\Bundle\DataTableBundle\Exporter\ExporterInterface;
+use Kreyu\Bundle\DataTableBundle\Exporter\ExportFile;
+use Kreyu\Bundle\DataTableBundle\DataTableView;
+
+$builder
+ ->addExporter('txt', CallbackExporterType::class, [
+ 'callback' => function (DataTableView $view, ExporterInterface $exporter, string $filename): ExportFile {
+ // ...
+ },
+ ])
+;
+```
+
+## Inherited options
+
+
diff --git a/docs/src/reference/types/exporter/exporter.md b/docs/src/reference/types/exporter/exporter.md
new file mode 100644
index 00000000..b5c0515b
--- /dev/null
+++ b/docs/src/reference/types/exporter/exporter.md
@@ -0,0 +1,11 @@
+
+
+# ExporterType
+
+The [`ExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/CallbackExporterType.php) represents a base exporter used as a parent for every other exporter type in the bundle.
+
+## Options
+
+
diff --git a/docs/src/reference/types/exporter/open-spout/csv.md b/docs/src/reference/types/exporter/open-spout/csv.md
new file mode 100644
index 00000000..cff62d1f
--- /dev/null
+++ b/docs/src/reference/types/exporter/open-spout/csv.md
@@ -0,0 +1,41 @@
+
+
+# OpenSpout CsvExporterType
+
+The [`CsvExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/OpenSpout/Exporter/Type/CsvExporterType.php) represents an exporter that uses an [OpenSpout](https://github.com/openspout/openspout) CSV writer.
+
+## Options
+
+### `field_delimiter`
+
+- **type**: `string`
+- **default**: `','`
+
+Represents a string that separates the values.
+
+### `field_enclosure`
+
+- **type**: `string`
+- **default**: `'"'`
+
+Represents a string that wraps the values.
+
+### `should_add_bom`
+
+- **type**: `bool`
+- **default**: `true`
+
+Determines whether a BOM character should be added at the beginning of the file.
+
+### `flush_threshold`
+
+- **type**: `int`
+- **default**: `500`
+
+Represents a number of rows after which the output should be flushed to a file.
+
+## Inherited options
+
+
\ No newline at end of file
diff --git a/docs/src/reference/types/exporter/open-spout/ods.md b/docs/src/reference/types/exporter/open-spout/ods.md
new file mode 100644
index 00000000..b1c458b1
--- /dev/null
+++ b/docs/src/reference/types/exporter/open-spout/ods.md
@@ -0,0 +1,42 @@
+
+
+# OpenSpout OdsExporterType
+
+The [`OdsExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/OpenSpout/Exporter/Type/OdsExporterType.php) represents an exporter that uses an [OpenSpout](https://github.com/openspout/openspout) ODS writer.
+
+## Options
+
+### `default_row_style`
+
+- **type**: `OpenSpout\Common\Entity\Style\Style`
+- **default**: an unmodified instance of `Style` class
+
+An instance of style class that will be applied to all rows.
+
+### `should_create_new_sheets_automatically`
+
+- **type**: `bool`
+- **default**: `true`
+
+Determines whether new sheets should be created automatically
+when the maximum number of rows (1,048,576) per sheet is reached.
+
+### `default_column_width`
+
+- **type**: `null` or `float`
+- **default**: `null`
+
+Represents a width that will be applied to all columns by default.
+
+### `default_row_height`
+
+- **type**: `null` or `float`
+- **default**: `null`
+
+Represents a height that will be applied to all rows by default.
+
+## Inherited options
+
+
\ No newline at end of file
diff --git a/docs/src/reference/types/exporter/open-spout/xlsx.md b/docs/src/reference/types/exporter/open-spout/xlsx.md
new file mode 100644
index 00000000..8c16b624
--- /dev/null
+++ b/docs/src/reference/types/exporter/open-spout/xlsx.md
@@ -0,0 +1,51 @@
+
+
+# OpenSpout XlsxExporterType
+
+The [`XlsxExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/OpenSpout/Exporter/Type/XlsxExporterType.php) represents an exporter that uses an [OpenSpout](https://github.com/openspout/openspout) XLSX writer.
+
+## Options
+
+### `default_row_style`
+
+- **type**: `OpenSpout\Common\Entity\Style\Style`
+- **default**: an unmodified instance of `Style` class
+
+An instance of style class that will be applied to all rows.
+
+### `should_create_new_sheets_automatically`
+
+- **type**: `bool`
+- **default**: `true`
+
+Determines whether new sheets should be created automatically
+when the maximum number of rows (1,048,576) per sheet is reached.
+
+### `should_use_inline_strings`
+
+- **type**: `bool`
+- **default**: `true`
+
+Determines whether inline strings should be used instead of shared strings.
+
+For more information about this configuration, see [OpenSpout documentation](https://github.com/openspout/openspout/blob/4.x/docs/documentation.md#strings-storage-xlsx-writer).
+
+### `default_column_width`
+
+- **type**: `null` or `float`
+- **default**: `null`
+
+Represents a width that will be applied to all columns by default.
+
+### `default_row_height`
+
+- **type**: `null` or `float`
+- **default**: `null`
+
+Represents a height that will be applied to all rows by default.
+
+## Inherited options
+
+
\ No newline at end of file
diff --git a/docs/src/reference/types/exporter/options/exporter.md b/docs/src/reference/types/exporter/options/exporter.md
new file mode 100644
index 00000000..b9d2eb67
--- /dev/null
+++ b/docs/src/reference/types/exporter/options/exporter.md
@@ -0,0 +1,27 @@
+### `use_headers`
+
+- **type**: `bool`
+- **default**: `true`
+
+Determines whether the exporter should add headers to the output file.
+
+### `label`
+
+- **type**: `null` or `string`
+- **default**: `null` the label is "guessed" from the exporter name
+
+Sets the label of the exporter, visible in the export action modal.
+
+### `tempnam_dir`
+
+- **type**: `string`
+- **default**: the value returned by the `sys_get_temp_dir()` function
+
+Sets the directory used to store temporary file during the export process.
+
+### `tempnam_prefix`
+
+- **type**: `string`
+- **default**: `exporter_`
+
+Sets the prefix used to generate temporary file names during the export process.
\ No newline at end of file
diff --git a/docs/src/reference/types/exporter/options/php-spreadsheet.md b/docs/src/reference/types/exporter/options/php-spreadsheet.md
new file mode 100644
index 00000000..f59ae356
--- /dev/null
+++ b/docs/src/reference/types/exporter/options/php-spreadsheet.md
@@ -0,0 +1,8 @@
+### `pre_calculate_formulas`
+
+- **type**: `bool`
+- **default**: `true`
+
+By default, the PhpSpreadsheet writers pre-calculates all formulas in the spreadsheet.
+This can be slow on large spreadsheets, and maybe even unwanted.
+The value of this option determines whether the formula pre-calculation is enabled.
diff --git a/docs/src/reference/types/exporter/php-spreadsheet/csv.md b/docs/src/reference/types/exporter/php-spreadsheet/csv.md
new file mode 100644
index 00000000..c7ee8f46
--- /dev/null
+++ b/docs/src/reference/types/exporter/php-spreadsheet/csv.md
@@ -0,0 +1,94 @@
+
+
+# PhpSpreadsheet CsvExporterType
+
+The [`CsvExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/CsvExporterType.php) represents an exporter that uses an [PhpSpreadsheet](https://github.com/PHPOffice/PhpSpreadsheet) CSV writer.
+
+## Options
+
+### `delimiter`
+
+**type**: `string` **default**: `','`
+
+Represents a string that separates the CSV files values.
+
+### `enclosure`
+
+**type**: `string` **default**: `'"'`
+
+Represents a string that wraps all CSV fields.
+
+### `enclosure_required`
+
+**type**: `bool` **default**: `true`
+
+By default, all CSV fields are wrapped in the enclosure character.
+Value of this option determines whether to use the enclosure character only when required.
+
+### `line_ending`
+
+**type**: `string` **default**: platform `PHP_EOL` constant value
+
+Represents a string that separates the CSV files lines.
+
+### `sheet_index`
+
+**type**: `int` **default**: `0`
+
+CSV files can only contain one worksheet. Therefore, you can specify which sheet to write to CSV.
+
+### `use_bom`
+
+**type**: `string` **default**: `false`
+
+CSV files are written in UTF-8. If they do not contain characters outside the ASCII range, nothing else need be done.
+However, if such characters are in the file, or if the file starts with the 2 characters 'ID', it should explicitly include a BOM file header;
+if it doesn't, Excel will not interpret those characters correctly. This can be enabled by setting this option to `true`.
+
+### `include_separator_line`
+
+**type**: `bool` **default**: `false`
+
+Determines whether a separator line should be included as the first line of the file.
+
+### `excel_compatibility`
+
+**type**: `bool` **default**: `false`
+
+Determines whether the file should be saved with full Excel compatibility.
+
+Note that this overrides other settings such as useBOM, enclosure and delimiter!
+
+### `output_encoding`
+
+**type**: `string` **default**: `''`
+
+It can be set to output with the encoding that can be specified by PHP's `mb_convert_encoding` (e.g. `'SJIS-WIN'`).
+
+### `decimal_separator`
+
+**type**: `string` **default**: depends on the server's locale setting
+
+If the worksheet you are exporting contains numbers with decimal separators,
+then you should think about what characters you want to use for those before doing the export.
+
+By default, PhpSpreadsheet looks up in the server's locale settings to decide what character to use.
+But to avoid problems it is recommended to set the character explicitly.
+
+### `thousands_separator`
+
+**type**: `string` **default**: depends on the server's locale setting
+
+If the worksheet you are exporting contains numbers with thousands separators,
+then you should think about what characters you want to use for those before doing the export.
+
+By default, PhpSpreadsheet looks up in the server's locale settings to decide what character to use.
+But to avoid problems it is recommended to set the character explicitly.
+
+## Inherited options
+
+
+
\ No newline at end of file
diff --git a/docs/src/reference/types/exporter/php-spreadsheet/html.md b/docs/src/reference/types/exporter/php-spreadsheet/html.md
new file mode 100644
index 00000000..6f4dfd01
--- /dev/null
+++ b/docs/src/reference/types/exporter/php-spreadsheet/html.md
@@ -0,0 +1,120 @@
+
+
+# PhpSpreadsheet HtmlExporterType
+
+The [`HtmlExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/HtmlExporterType.php) represents an exporter that uses an [PhpSpreadsheet](https://github.com/PHPOffice/PhpSpreadsheet) HTML writer.
+
+## Options
+
+### `sheet_index`
+
+- **type**: `null` or `int`
+- **default**: `0`
+
+HTML files can only contain one or more worksheets.
+Therefore, you can specify which sheet to write to HTML.
+If you want to write all sheets into a single HTML file, set this option to `null`.
+
+### `images_root`
+
+- **type**: `string`
+- **default**: `''`
+
+There might be situations where you want to explicitly set the included images root. For example, instead of:
+
+```html
+
+```
+
+You might want to see:
+
+```html
+
+```
+
+Use this option to achieve this result:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Bridge\PhpSpreadsheet\Exporter\Type\HtmlExporterType;
+
+$builder
+ ->addExporter('html', HtmlExporterType::class, [
+ 'images_root' => 'https://www.domain.com',
+ ])
+;
+```
+
+### `embed_images`
+
+- **type**: `bool`
+- **default**: `false`
+
+Determines whether the images should be embedded or not.
+
+### `use_inline_css`
+
+- **type**: `bool`
+- **default**: `false`
+
+Determines whether the inline css should be used or not.
+
+### `generate_sheet_navigation_block`
+
+- **type**: `bool`
+- **default**: `true`
+
+Determines whether the sheet navigation block should be generated or not.
+
+### `edit_html_callback`
+
+- **type**: `null` or `callable`
+- **default**: `null`
+
+Accepts a callback function to edit the generated html before saving.
+For example, you could change the gridlines from a thin solid black line:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Bridge\PhpSpreadsheet\Exporter\Type\HtmlExporterType;
+
+$builder
+ ->addExporter('html', HtmlExporterType::class, [
+ 'edit_html_callback' => function (string $html): string {
+ return str_replace(
+ '{border: 1px solid black;}',
+ '{border: 2px dashed red;}',
+ $html,
+ );
+ }
+ ])
+;
+```
+
+### `decimal_separator`
+
+- **type**: `string`
+- **default**: depends on the server's locale setting
+
+If the worksheet you are exporting contains numbers with decimal separators,
+then you should think about what characters you want to use for those before doing the export.
+
+By default, PhpSpreadsheet looks up in the server's locale settings to decide what character to use.
+But to avoid problems, it is recommended to set the character explicitly.
+
+### `thousands_separator`
+
+- **type**: `string`
+- **default**: depends on the server's locale setting
+
+If the worksheet you are exporting contains numbers with thousands separators,
+then you should think about what characters you want to use for those before doing the export.
+
+By default, PhpSpreadsheet looks up in the server's locale settings to decide what character to use.
+But to avoid problems, it is recommended to set the character explicitly.
+
+## Inherited options
+
+
+
diff --git a/docs/src/reference/types/exporter/php-spreadsheet/ods.md b/docs/src/reference/types/exporter/php-spreadsheet/ods.md
new file mode 100644
index 00000000..bf069efb
--- /dev/null
+++ b/docs/src/reference/types/exporter/php-spreadsheet/ods.md
@@ -0,0 +1,17 @@
+
+
+# PhpSpreadsheet OdsExporterType
+
+The [`XlsxExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/XlsxExporterType.php) represents an exporter that uses an [PhpSpreadsheet](https://github.com/PHPOffice/PhpSpreadsheet) ODS writer.
+
+## Options
+
+This exporter type has no additional options.
+
+## Inherited options
+
+
+
diff --git a/docs/src/reference/types/exporter/php-spreadsheet/pdf.md b/docs/src/reference/types/exporter/php-spreadsheet/pdf.md
new file mode 100644
index 00000000..cba7fe7b
--- /dev/null
+++ b/docs/src/reference/types/exporter/php-spreadsheet/pdf.md
@@ -0,0 +1,120 @@
+
+
+# PhpSpreadsheet PdfExporterType
+
+The [`PdfExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/PdfExporterType.php) represents an exporter that uses an [PhpSpreadsheet](https://github.com/PHPOffice/PhpSpreadsheet) PDF writer.
+
+## Options
+
+### `sheet_index`
+
+- **type**: `null` or `int`
+- **default**: `0`
+
+PDF files can only contain one or more worksheets.
+Therefore, you can specify which sheet to write to PDF.
+If you want to write all sheets into a single PDF file, set this option to `null`.
+
+### `images_root`
+
+- **type**: `string`
+- **default**: `''`
+
+There might be situations where you want to explicitly set the included images root. For example, instead of:
+
+```html
+
+```
+
+You might want to see:
+
+```html
+
+```
+
+Use this option to achieve this result:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Bridge\PhpSpreadsheet\Exporter\Type\PdfExporterType;
+
+$builder
+ ->addExporter('html', PdfExporterType::class, [
+ 'images_root' => 'https://www.domain.com',
+ ])
+;
+```
+
+### `embed_images`
+
+- **type**: `bool`
+- **default**: `false`
+
+Determines whether the images should be embedded or not.
+
+### `use_inline_css`
+
+- **type**: `bool`
+- **default**: `false`
+
+Determines whether the inline css should be used or not.
+
+### `generate_sheet_navigation_block`
+
+- **type**: `bool`
+- **default**: `true`
+
+Determines whether the sheet navigation block should be generated or not.
+
+### `edit_html_callback`
+
+- **type**: `null` or `callable`
+- **default**: `null`
+
+Accepts a callback function to edit the generated html before saving.
+For example, you could change the gridlines from a thin solid black line:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Bridge\PhpSpreadsheet\Exporter\Type\PdfExporterType;
+
+$builder
+ ->addExporter('html', PdfExporterType::class, [
+ 'edit_html_callback' => function (string $html): string {
+ return str_replace(
+ '{border: 1px solid black;}',
+ '{border: 2px dashed red;}',
+ $html,
+ );
+ }
+ ])
+;
+```
+
+### `decimal_separator`
+
+- **type**: `string`
+- **default**: depends on the server's locale setting
+
+If the worksheet you are exporting contains numbers with decimal separators,
+then you should think about what characters you want to use for those before doing the export.
+
+By default, PhpSpreadsheet looks up in the server's locale settings to decide what character to use.
+But to avoid problems, it is recommended to set the character explicitly.
+
+### `thousands_separator`
+
+- **type**: `string`
+- **default**: depends on the server's locale setting
+
+If the worksheet you are exporting contains numbers with thousands separators,
+then you should think about what characters you want to use for those before doing the export.
+
+By default, PhpSpreadsheet looks up in the server's locale settings to decide what character to use.
+But to avoid problems, it is recommended to set the character explicitly.
+
+## Inherited options
+
+
+
diff --git a/docs/src/reference/types/exporter/php-spreadsheet/xls.md b/docs/src/reference/types/exporter/php-spreadsheet/xls.md
new file mode 100644
index 00000000..fd908f7d
--- /dev/null
+++ b/docs/src/reference/types/exporter/php-spreadsheet/xls.md
@@ -0,0 +1,17 @@
+
+
+# PhpSpreadsheet XlsExporterType
+
+The [`XlsxExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/XlsxExporterType.php) represents an exporter that uses an [PhpSpreadsheet](https://github.com/PHPOffice/PhpSpreadsheet) XLS writer.
+
+## Options
+
+This exporter type has no additional options.
+
+## Inherited options
+
+
+
diff --git a/docs/src/reference/types/exporter/php-spreadsheet/xlsx.md b/docs/src/reference/types/exporter/php-spreadsheet/xlsx.md
new file mode 100644
index 00000000..e2373e68
--- /dev/null
+++ b/docs/src/reference/types/exporter/php-spreadsheet/xlsx.md
@@ -0,0 +1,23 @@
+
+
+# PhpSpreadsheet XlsxExporterType
+
+The [`XlsxExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/XlsxExporterType.php) represents an exporter that uses an [PhpSpreadsheet](https://github.com/PHPOffice/PhpSpreadsheet) XLSX writer.
+
+## Options
+
+### `office_2003_compatibility`
+
+- **type**: `bool`
+- **default**: `false`
+
+Because of a bug in the Office2003 compatibility pack, there can be some small issues when opening
+Xlsx spreadsheets (mostly related to formula calculation). You can enable Office2003 compatibility by setting this option to `true`.
+
+## Inherited options
+
+
+
diff --git a/docs/src/reference/types/filter.md b/docs/src/reference/types/filter.md
new file mode 100644
index 00000000..99d13f0d
--- /dev/null
+++ b/docs/src/reference/types/filter.md
@@ -0,0 +1,20 @@
+# Filter types
+
+The following filter types are natively available in the bundle:
+
+- [Callback](types/callback.md)
+- [Search](types/search.md)
+- [Filter](types/filter.md)
+
+## Doctrine ORM
+
+The built-in Doctrine ORM integration provides additional filter types:
+
+- [String](filter/doctrine-orm/string.md)
+- [Numeric](filter/doctrine-orm/numeric.md)
+- [Boolean](filter/doctrine-orm/boolean.md)
+- [Date](filter/doctrine-orm/date.md)
+- [DateTime](filter/doctrine-orm/date-time.md)
+- [DateRange](filter/doctrine-orm/date-range.md)
+- [Entity](filter/doctrine-orm/entity.md)
+- [DoctrineOrm](filter/doctrine-orm/doctrine-orm.md)
diff --git a/docs/src/reference/types/filter/callback.md b/docs/src/reference/types/filter/callback.md
new file mode 100644
index 00000000..22502970
--- /dev/null
+++ b/docs/src/reference/types/filter/callback.md
@@ -0,0 +1,34 @@
+
+
+# CallbackFilterType
+
+The [`CallbackFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/CallbackFilterType.php) represents a filter that uses a given callback as its handler.
+
+## Options
+
+### `callback`
+
+- **type**: `callable`
+
+Sets callable that works as a filter handler.
+
+```php
+use Kreyu\Bundle\DataTableBundle\Filter\Type\CallbackFilterType;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
+use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
+
+$builder
+ ->addFilter('name', CallbackFilterType::class, [
+ 'callback' => function (ProxyQueryInterface $query, FilterData $data, FilterInterface $filter): void {
+ // ...
+ },
+ ])
+;
+```
+
+## Inherited options
+
+
diff --git a/docs/src/reference/types/filter/doctrine-orm/boolean.md b/docs/src/reference/types/filter/doctrine-orm/boolean.md
new file mode 100644
index 00000000..d906ffd9
--- /dev/null
+++ b/docs/src/reference/types/filter/doctrine-orm/boolean.md
@@ -0,0 +1,21 @@
+
+
+# Doctrine ORM BooleanFilterType
+
+The Doctrine ORM [`BooleanFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/Type/BooleanFilterType.php) represents a filter that operates on a boolean values.
+
+## Options
+
+This column type has no additional options.
+
+## Inherited options
+
+
+
+
diff --git a/docs/src/reference/types/filter/doctrine-orm/date-range.md b/docs/src/reference/types/filter/doctrine-orm/date-range.md
new file mode 100644
index 00000000..431e32e9
--- /dev/null
+++ b/docs/src/reference/types/filter/doctrine-orm/date-range.md
@@ -0,0 +1,21 @@
+
+
+# Doctrine ORM DateRangeFilterType
+
+The Doctrine ORM [`DateRangeFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/Type/DateRangeFilterType.php) represents a filter that operates on a two date values that make a range.
+
+## Options
+
+This column type has no additional options.
+
+## Inherited options
+
+
+
+
\ No newline at end of file
diff --git a/docs/src/reference/types/filter/doctrine-orm/date-time.md b/docs/src/reference/types/filter/doctrine-orm/date-time.md
new file mode 100644
index 00000000..c9ee54d3
--- /dev/null
+++ b/docs/src/reference/types/filter/doctrine-orm/date-time.md
@@ -0,0 +1,21 @@
+
+
+# Doctrine ORM DateTimeFilterType
+
+The Doctrine ORM [`DateTimeFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/Type/DateTimeFilterType.php) represents a filter that operates on a date time values.
+
+## Options
+
+This column type has no additional options.
+
+## Inherited options
+
+
+
+
\ No newline at end of file
diff --git a/docs/src/reference/types/filter/doctrine-orm/date.md b/docs/src/reference/types/filter/doctrine-orm/date.md
new file mode 100644
index 00000000..f0dd5a89
--- /dev/null
+++ b/docs/src/reference/types/filter/doctrine-orm/date.md
@@ -0,0 +1,21 @@
+
+
+# Doctrine ORM DateFilterType
+
+The Doctrine ORM [`DateFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/Type/DateFilterType.php) represents a filter that operates on a date (without time) values.
+
+## Options
+
+This column type has no additional options.
+
+## Inherited options
+
+
+
+
\ No newline at end of file
diff --git a/docs/src/reference/types/filter/doctrine-orm/doctrine-orm.md b/docs/src/reference/types/filter/doctrine-orm/doctrine-orm.md
new file mode 100644
index 00000000..b71e3a8d
--- /dev/null
+++ b/docs/src/reference/types/filter/doctrine-orm/doctrine-orm.md
@@ -0,0 +1,16 @@
+
+
+# DoctrineOrmFilterType
+
+The Doctrine ORM [`DoctrineOrmFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/Type/DoctrineOrmFilterType.php) represents a base type that every other filter type from the integration uses as a parent.
+
+## Options
+
+
+
+## Inherited options
+
+
diff --git a/docs/src/reference/types/filter/doctrine-orm/entity.md b/docs/src/reference/types/filter/doctrine-orm/entity.md
new file mode 100644
index 00000000..a5d213da
--- /dev/null
+++ b/docs/src/reference/types/filter/doctrine-orm/entity.md
@@ -0,0 +1,76 @@
+
+
+# Doctrine ORM EntityFilterType
+
+The Doctrine ORM [`EntityFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/Type/EntityFilterType.php) represents a filter that operates on an entity values.
+
+## Options
+
+### `choice_label`
+
+This is the property that should be used for displaying the entities as text in the active filter HTML element.
+
+```php
+use App\Entity\Category;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\EntityFilterType;
+// ...
+
+$builder->addFilter('category', EntityFilterType::class, [
+ 'form_options' => [
+ 'class' => Category::class,
+ 'choice_label' => 'displayName', // choice label for form
+ ],
+ 'choice_label' => 'displayName', // separate choice label for data table filter
+]);
+```
+
+If left blank, the entity object will be cast to a string and so must have a `__toString()` method. You can also pass a callback function for more control:
+
+```php
+use App\Entity\Category;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\EntityFilterType;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
+// ...
+
+$builder->addFilter('category', EntityFilterType::class, [
+ 'form_options' => [
+ 'class' => Category::class,
+ 'choice_label' => 'displayName',
+ ],
+ 'choice_label' => function (FilterData $data): string {
+ return $data->getValue()->getDisplayName();
+ },
+]);
+```
+
+::: tip This option works like a `choice_label` option in `ChoiceType` form option.
+When passing a string, the `choice_label` option is a property path. So you can use anything supported by the [PropertyAccess component](https://symfony.com/doc/current/components/property_access.html).
+
+For example, if the translations property is actually an associative array of objects, each with a name property, then you could do this:
+
+```php
+use App\Entity\Category;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\EntityFilterType;
+// ...
+
+$builder->addFilter('category', EntityFilterType::class, [
+ 'form_options' => [
+ 'class' => Category::class,
+ 'choice_label' => 'translations[en].name',
+ ],
+ 'choice_label' => 'translations[en].name',
+]);
+```
+:::
+
+## Inherited options
+
+
+
+
\ No newline at end of file
diff --git a/docs/src/reference/types/filter/doctrine-orm/numeric.md b/docs/src/reference/types/filter/doctrine-orm/numeric.md
new file mode 100644
index 00000000..760fdf32
--- /dev/null
+++ b/docs/src/reference/types/filter/doctrine-orm/numeric.md
@@ -0,0 +1,20 @@
+
+
+# Doctrine ORM NumericFilterType
+
+The Doctrine ORM [`NumericFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/Type/NumericFilterType.php) represents a filter that operates on a numeric values.
+
+## Options
+
+This column type has no additional options.
+
+## Inherited options
+
+
+
+
\ No newline at end of file
diff --git a/docs/src/reference/types/filter/doctrine-orm/string.md b/docs/src/reference/types/filter/doctrine-orm/string.md
new file mode 100644
index 00000000..2aee1119
--- /dev/null
+++ b/docs/src/reference/types/filter/doctrine-orm/string.md
@@ -0,0 +1,20 @@
+
+
+# Doctrine ORM StringFilterType
+
+The Doctrine ORM [`StringFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/Type/StringFilterType.php) represents a filter that operates on a string values.
+
+## Options
+
+This column type has no additional options.
+
+## Inherited options
+
+
+
+
\ No newline at end of file
diff --git a/docs/src/reference/types/filter/filter.md b/docs/src/reference/types/filter/filter.md
new file mode 100644
index 00000000..ef8fbbee
--- /dev/null
+++ b/docs/src/reference/types/filter/filter.md
@@ -0,0 +1,11 @@
+
+
+# FilterType
+
+The [`FilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/FilterType.php) represents a base filter used as a parent for every other filter type in the bundle.
+
+## Options
+
+
diff --git a/docs/src/reference/types/filter/options/doctrine-orm.md b/docs/src/reference/types/filter/options/doctrine-orm.md
new file mode 100644
index 00000000..8f000df9
--- /dev/null
+++ b/docs/src/reference/types/filter/options/doctrine-orm.md
@@ -0,0 +1,53 @@
+### `trim`
+
+- **type**: `bool`
+- **default**: `false`
+
+Determines whether the `TRIM()` function should be applied on the expression. Uses the [`TrimExpressionTransformer`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/TrimExpressionTransformer.php) transformer.
+
+### `lower`
+
+- **type**: `bool`
+- **default**: `false`
+
+Determines whether the `LOWER()` function should be applied on the expression. Uses the [`LowerExpressionTransformer`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/LowerExpressionTransformer.php) transformer.
+
+### `upper`
+
+- **type**: `bool`
+- **default**: `false`
+
+Determines whether the `UPPER()` function should be applied on the expression. Uses the [`UpperExpressionTransformer`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/UpperExpressionTransformer.php) transformer.
+
+### `expression_transformers`
+
+- **type**: [`ExpressionTransformerInterface[]`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/ExpressionTransformerInterface.php)
+- **default**: `[]`
+
+Defines expression transformers to apply on the expression.
+
+```php
+use App\DataTable\Filter\ExpressionTransformer\UnaccentExpressionTransformer;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\ExpressionTransformer\LowerExpressionTransformer;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\ExpressionTransformer\TrimExpressionTransformer;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addFilter('name', TextFilterType::class, [
+ 'expression_transformers' => [
+ new LowerExpressionTransformer(),
+ new TrimExpressionTransformer(),
+ ],
+ ])
+ ;
+ }
+}
+```
+
+For more information about expression transformers, [read here](../../../../../docs/integrations/doctrine-orm/expression-transformers.md).
diff --git a/docs/src/reference/types/filter/options/filter.md b/docs/src/reference/types/filter/options/filter.md
new file mode 100644
index 00000000..b9c1a43f
--- /dev/null
+++ b/docs/src/reference/types/filter/options/filter.md
@@ -0,0 +1,90 @@
+
+
+### `label`
+
+- **type**: `null`, `false`, `string` or `Symfony\Component\Translation\TranslatableInterface`
+- **default**: `null`
+
+Sets the label that will be used when rendering the filter.
+
+When value is `null`, a sentence cased filter name is used as a label, for example:
+
+| Filter name | Guessed label |
+|-------------|---------------|
+| name | Name |
+| firstName | First name |
+
+### `label_translation_parameters`
+
+- **type**: `array`
+- **default**: `[]`
+
+Sets the parameters used when translating the `label` option.
+
+### `translation_domain`
+
+- **type**: `false` or `string`
+- **default**: `'KreyuDataTable'`
+
+Sets the translation domain used when translating the translatable filter values.
+Setting the option to `false` disables translation for the filter.
+
+### `query_path`
+
+- **type**: `null` or `string`
+- **default**: `null` the query path is guessed from the filter name
+
+Sets the path used in the proxy query to perform the filtering on.
+
+### `form_type`
+
+- **type**: `string`
+- **default**: `'{{ defaults.formType }}'`
+
+This is the form type used to render the filter value field.
+
+### `form_options`
+
+- **type**: `array`
+- **default**: `{{ defaults.formOptions }}`
+
+This is the array that's passed to the form type specified in the `form_type` option.
+
+### `operator_form_type`
+
+- **type**: `string`
+- **default**: `'Kreyu\Bundle\DataTableBundle\Filter\Form\Type\OperatorType'`
+
+This is the form type used to render the filter operator field.
+
+### `operator_form_options`
+
+- **type**: `array`
+- **default**: `[]`
+
+This is the array that's passed to the form type specified in the `operator_form_type` option.
+
+### `operator_selectable`
+
+- **type**: `bool`
+- **default**: `false`
+
+Determines whether the operator can be selected by the user.
+
+### `default_operator`
+
+- **type**: `Kreyu\Bundle\DataTableBundle\Filter\Operator`
+- **default**: `{{ defaults.defaultOperator }}`
+
+Determines a default operator for the filter.
diff --git a/docs/src/reference/types/filter/search.md b/docs/src/reference/types/filter/search.md
new file mode 100644
index 00000000..fa644d02
--- /dev/null
+++ b/docs/src/reference/types/filter/search.md
@@ -0,0 +1,91 @@
+
+
+# SearchFilterType
+
+The [`SearchFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/SearchFilterType.php) represents a special filter that is rendered on the outside of filtering form as a search input.
+
+![Search filter type](/search_filter_type.png)
+
+## Adding the search handler
+
+Instead of using this filter, you can use the `setSearchHandler()` method:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder->setSearchHandler(function (ProxyQueryInterface $query, string $search) {
+ // ...
+ });
+ }
+}
+```
+
+Defining a search handler automatically adds search filter.
+
+To disable this behavior, use the `setAutoAddingSearchFilter()` method:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder->setAutoAddingSearchFilter(false);
+ }
+}
+```
+
+To override configuration of the automatically added filter, you can add search filter manually with the same name:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
+use Kreyu\Bundle\DataTableBundle\Filter\Type\SearchFilterType;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ // Set "__search" as filter name or use constant:
+ $builder->addFilter(DataTableBuilderInterface::SEARCH_FILTER_NAME, SearchFilterType::class, [
+ // ...
+ ]);
+ }
+}
+```
+
+## Options
+
+### `handler`
+
+- **type**: `callable`
+
+Sets callable that operates on the query passed as a first argument:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Filter\Type\SearchFilterType;
+use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
+
+$builder
+ ->addFilter('search', SearchFilterType::class, [
+ 'handler' => function (ProxyQueryInterface $query, string $search): void {
+ // ...
+ },
+ ])
+```
+
+## Inherited options
+
+
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 8a9f03c4..7c32da77 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -8,6 +8,9 @@
tests
+
+
+