diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..21256661
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto
\ No newline at end of file
diff --git a/.github/workflows/retype-action.yml b/.github/workflows/retype-action.yml
old mode 100644
new mode 100755
diff --git a/.gitignore b/.gitignore
old mode 100644
new mode 100755
index 9cb4a051..33c9215b
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,6 @@
/venv
/composer.lock
/.php-cs-fixer.cache
+/.phpunit.result.cache
/.idea
/.vscode
\ No newline at end of file
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
old mode 100644
new mode 100755
diff --git a/CHANGELOG.md b/CHANGELOG.md
old mode 100644
new mode 100755
index c14a14e2..44b7815a
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,29 @@
+# 0.14
+
+- **[Feature]** Data table events
+- **[Feature]** Column `priority` option to allow setting order of columns
+- **[Feature]** Column `visible` option to allow setting visibility of columns
+- **[Feature]** Column `personalizable` option to allow excluding the column from personalization
+- **[Feature]** More verbose filter type form-related options such as `form_type`, `operator_form_type`
+- **[Feature]** Ability to set hydration mode of the Doctrine ORM proxy query
+- **[Feature]** Data table builder's `setSearchHandler` method for easier search definition
+- **[Feature]** The `CollectionColumnType` default separator changed `', '` (with space after comma) instead of `','`
+- **[Feature]** Ability to create `ExportData` with exporter name string
+- **[Feature]** Ability to provide property path in the `SortingColumnData`. The data table ensures valid property path is given (backwards compatible)
+- **[Feature]** The Doctrine ORM `EntityFilterType` no longer requires `form_options.choice_value` option as the identifier field name will be retrieved from Doctrine class metadata by default
+- **[Feature]** The `DateColumnType` that works exactly like `DateTimeColumnType`, but with date-only format by default
+- **[Breaking change]** The data table type persistence subject options are removed in favor of subject provider options (see more)
+- **[Breaking change]** Optimized exporting process - introduces breaking changes (see more)
+- **[Breaking change]** The `DataTableBuilder` methods to add columns, filters, actions and exporters has changed definition - the `type` argument is now nullable to prepare for future implementation of type guessers
+- **[Bugfix]** Fixed a bug in personalization form where changing the column visibility resulted in an exception
+- **[Bugfix]** The `CollectionColumnType` now renders without spaces around separator
+- **[Bugfix]** Default export data is now properly used within the export form
+
+Internally, the columns, filters and exporters are now utilizing the builder pattern similar to data tables.
+Please note that this is a **breaking change** for applications using internal bundle classes!
+
+For a list of all breaking changes and deprecations, see the [upgrade guide](docs/upgrade-guide/0.14.md).
+
# 0.13
- **[Feature]** Batch actions ([see more](https://data-table-bundle.swroblewski.pl/features/actions/batch-actions/))
@@ -7,3 +33,4 @@
- **[Feature]** MoneyColumnType with optional Intl integration ([see more](https://data-table-bundle.swroblewski.pl/reference/columns/types/money/))
Internally, the actions are now utilizing the builder pattern similar to data tables.
+Please note that this is a **breaking change** for applications using internal bundle classes!
diff --git a/LICENSE b/LICENSE
old mode 100644
new mode 100755
index e9158150..7a46d7cf
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2023 Sebastian Wróblewski
+Copyright (c) 2023-present Sebastian Wróblewski
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
old mode 100644
new mode 100755
diff --git a/assets/controllers/batch.js b/assets/controllers/batch.js
old mode 100644
new mode 100755
diff --git a/assets/controllers/personalization.js b/assets/controllers/personalization.js
old mode 100644
new mode 100755
index a448ccfe..bf8fa19a
--- a/assets/controllers/personalization.js
+++ b/assets/controllers/personalization.js
@@ -4,13 +4,21 @@ import Sortable from 'sortablejs'
export default class extends Controller {
static targets = [ 'visibleColumns', 'hiddenColumns' ]
+ #visibleColumnsSortable = null
+ #hiddenColumnSortable = null
+
connect() {
- this.#initializeSortable(this.visibleColumnsTarget)
- this.#initializeSortable(this.hiddenColumnsTarget)
+ this.#visibleColumnsSortable = this.#initializeSortable(this.visibleColumnsTarget)
+ this.#hiddenColumnSortable = this.#initializeSortable(this.hiddenColumnsTarget)
+ }
+
+ disconnect() {
+ this.#visibleColumnsSortable.destroy();
+ this.#hiddenColumnSortable.destroy();
}
#initializeSortable(target) {
- new Sortable(target, {
+ return new Sortable(target, {
group: 'shared',
animation: 150,
onAdd: event => {
@@ -19,12 +27,16 @@ export default class extends Controller {
input.value = event.to.dataset.visible
},
onChange: event => {
- const orderInput = event.item.querySelector('[name$="[order]"]')
- const originalOrderInput = event.originalEvent.target.querySelector('[name$="[order]"]')
+ const priorityInput = event.item.querySelector('[name$="[priority]"]')
+ const originalPriorityInput = event.originalEvent.target.querySelector('[name$="[priority]"]')
- orderInput.value = event.newIndex
- originalOrderInput.value = event.oldIndex
+ priorityInput.value = this.#calculatePriority(target, event.newIndex)
+ originalPriorityInput.value = this.#calculatePriority(target, event.oldIndex)
}
})
}
+
+ #calculatePriority(target, index) {
+ return target.childElementCount - index - 1;
+ }
}
diff --git a/assets/package.json b/assets/package.json
old mode 100644
new mode 100755
diff --git a/composer.json b/composer.json
old mode 100644
new mode 100755
index 23be9c1c..c0ad0eba
--- a/composer.json
+++ b/composer.json
@@ -23,7 +23,8 @@
"twig/intl-extra": "^3.6"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^3.13",
+ "roave/security-advisories": "dev-latest",
+ "friendsofphp/php-cs-fixer": "^3.23",
"kubawerlos/php-cs-fixer-custom-fixers": "^3.11",
"symfony/maker-bundle": "^1.48",
"symfony/security-core": "^6.2",
diff --git a/docs/basic-usage/adding-actions.md b/docs/basic-usage/adding-actions.md
old mode 100644
new mode 100755
index e7f2ea57..7c1ba7ab
--- a/docs/basic-usage/adding-actions.md
+++ b/docs/basic-usage/adding-actions.md
@@ -122,7 +122,7 @@ For reference, see [built-in action types](../components/actions/types.md).
Let's assume that the application has an `app_product_show` route for showing details about specific product.
This route requires a product identifier, therefore it has to be a row action.
-To add batch action, use the builder's `addRowAction()` method:
+To add row action, use the builder's `addRowAction()` method:
```php # src/DataTable/Type/ProductDataTableType.php
use App\Entity\Product;
diff --git a/docs/basic-usage/adding-columns.md b/docs/basic-usage/adding-columns.md
old mode 100644
new mode 100755
diff --git a/docs/basic-usage/creating-data-tables.md b/docs/basic-usage/creating-data-tables.md
old mode 100644
new mode 100755
diff --git a/docs/basic-usage/defining-the-filters.md b/docs/basic-usage/defining-the-filters.md
old mode 100644
new mode 100755
diff --git a/docs/basic-usage/disclaimer.md b/docs/basic-usage/disclaimer.md
old mode 100644
new mode 100755
index 172723f4..1ddaa36b
--- a/docs/basic-usage/disclaimer.md
+++ b/docs/basic-usage/disclaimer.md
@@ -16,20 +16,37 @@ Instead, they contain links to the reference section, where you can about each f
The articles assume, that the project uses [Doctrine ORM](https://www.doctrine-project.org/projects/orm.html) and contains a Product entity:
```php # src/Entity/Product.php
+use Doctrine\ORM\Mapping as ORM;
+
+#[ORM\Entity]
class Product
{
+ #[ORM\Id, ORM\GeneratedValue, ORM\Column]
private int $id;
+
+ #[ORM\Column]
private string $name;
+
+ #[ORM\Column]
private \DateTimeInterface $createdAt;
- public function getId(): int {}
- public function getName(): string {}
- public function getCreatedAt(): \DateTimeInterface {}
+ public function getId(): int
+ {
+ return $this->id;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function getCreatedAt(): \DateTimeInterface
+ {
+ return $this->createdAt;
+ }
}
```
-For the sake of simplicity, the Doctrine mapping is skipped in the code block above.
-
## Frontend
The examples contain screenshots using the built-in [Tabler UI Kit](https://tabler.io/) theme.
diff --git a/docs/basic-usage/enabling-global-search.md b/docs/basic-usage/enabling-global-search.md
old mode 100644
new mode 100755
index 7fbbbfb9..cfb4f146
--- a/docs/basic-usage/enabling-global-search.md
+++ b/docs/basic-usage/enabling-global-search.md
@@ -12,68 +12,47 @@ Sometimes all the user needs is a single text input, to quickly search through m
To handle that, there's a built-in special filter, which allows doing exactly that.
The uniqueness of this filter shines in the way it is rendered - in the built-in themes, instead of showing up in the filter form, it gets displayed above, always visible, easily accessible.
-## Adding the search filter
+## Adding the search handler
-To start, define a filter of search type — its name doesn't really matter:
+To define a search handler, use the builder's `setSearchHandler()` method to provide a callable,
+which gets an instance of query, and a search string as its arguments:
```php # src/DataTable/Type/ProductDataTableType.php
use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
-
-class ProductDataTableType extends AbstractDataTableType
-{
- public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
- {
- // Columns and filters added before...
-
- $builder
- ->addFilter('search', SearchFilterType::class)
- ;
- }
-}
-```
-
-Trying to display the data table with this filter configuration will result in an error:
-
-> The required option "handler" is missing.
-
-This is because the search filter type requires a `handler` option, which contains all the logic required for the data table search capabilities.
-
-## Writing the search handler
-
-The option accepts a callable, which gets an instance of query, and a search string as its arguments:
-
-```php #12 src/DataTable/Type/ProductDataTableType.php
-use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
-use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQuery;
class ProductDataTableType extends AbstractDataTableType
{
public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
{
- // Columns and filters added before...
-
$builder
- ->addFilter('search', SearchFilterType::class, [
- 'handler' => $this->handleSearchFilter(...),
- ])
+ ->setSearchHandler($this->handleSearchFilter(...))
;
}
- /**
- * @param DoctrineOrmProxyQuery $query
- */
- private function handleSearchFilter(ProxyQueryInterface $query, string $search): void
+ private function handleSearchFilter(DoctrineOrmProxyQuery $query, string $search): void
{
+ $alias = current($query->getRootAliases());
+
+ // Remember to use parameters to prevent SQL Injection!
+ // To help with that, DoctrineOrmProxyQuery has a special method "getUniqueParameterId",
+ // that will generate a unique parameter name (inside its query context), handy!
+ $parameter = $query->getUniqueParameterId();
+
+ $query
+ ->andWhere($query->expr()->eq("$alias.type", ":$parameter"))
+ ->setParameter($parameter, $data->getValue())
+ ;
+
$criteria = $query->expr()->orX(
- $query->expr()->like('product.id', ':search'),
- $query->expr()->like('product.name', ':search'),
+ $query->expr()->like("$alias.id", ":$parameter"),
+ $query->expr()->like("$alias.name", ":$parameter"),
);
$query
->andWhere($criteria)
- ->setParameter('search', '%' . $search . '%')
+ ->setParameter($parameter, "%$search%")
;
}
}
diff --git a/docs/basic-usage/enabling-persistence.md b/docs/basic-usage/enabling-persistence.md
old mode 100644
new mode 100755
diff --git a/docs/basic-usage/exporting-the-data.md b/docs/basic-usage/exporting-the-data.md
old mode 100644
new mode 100755
diff --git a/docs/basic-usage/index.yml b/docs/basic-usage/index.yml
old mode 100644
new mode 100755
diff --git a/docs/basic-usage/internationalization.md b/docs/basic-usage/internationalization.md
old mode 100644
new mode 100755
diff --git a/docs/basic-usage/persisting-applied-data.md b/docs/basic-usage/persisting-applied-data.md
old mode 100644
new mode 100755
diff --git a/docs/basic-usage/rendering-the-table.md b/docs/basic-usage/rendering-the-table.md
old mode 100644
new mode 100755
index 0306104f..00c7c09c
--- a/docs/basic-usage/rendering-the-table.md
+++ b/docs/basic-usage/rendering-the-table.md
@@ -38,13 +38,13 @@ class ProductController extends AbstractController
Now, create the missing template, and render the data table:
-{%{
+{% raw %}
```twig # templates/product/index.html.twig
{{ data_table(data_table) }}
```
-}%}
+{% endraw %}
Voilà! :sparkles: The Twig helper function handles all the work and renders the data table.
diff --git a/docs/basic-usage/summary.md b/docs/basic-usage/summary.md
old mode 100644
new mode 100755
diff --git a/docs/contributing.md b/docs/contributing.md
new file mode 100644
index 00000000..1842b027
--- /dev/null
+++ b/docs/contributing.md
@@ -0,0 +1,18 @@
+---
+icon: heart
+
+---
+
+# Contributing
+
+## Documentation
+
+The documentation is powered by the [Retype](https://retype.com/). The articles are stored in the `docs/` directory.
+
+To locally preview the documentation, first, install the [Retype](https://retype.com/) locally.
+The installation instructions are available in the ["Getting Started" documentation section](https://retype.com/guides/getting-started/).
+Then, to build the documentation locally (and rebuild when change is detected), run the following command:
+
+```shell
+$ retype start docs
+```
diff --git a/docs/features/actions/actions.md b/docs/features/actions/actions.md
old mode 100644
new mode 100755
diff --git a/docs/features/actions/batch-actions.md b/docs/features/actions/batch-actions.md
old mode 100644
new mode 100755
diff --git a/docs/features/actions/global-actions.md b/docs/features/actions/global-actions.md
old mode 100644
new mode 100755
diff --git a/docs/features/actions/index.yml b/docs/features/actions/index.yml
new file mode 100644
index 00000000..e0daa2b4
--- /dev/null
+++ b/docs/features/actions/index.yml
@@ -0,0 +1 @@
+order: B
\ No newline at end of file
diff --git a/docs/features/actions/row-actions.md b/docs/features/actions/row-actions.md
old mode 100644
new mode 100755
diff --git a/docs/features/columns.md b/docs/features/columns.md
new file mode 100644
index 00000000..01f837d0
--- /dev/null
+++ b/docs/features/columns.md
@@ -0,0 +1,180 @@
+# Columns
+
+Columns are the main building blocks of a data tables, split into two parts: the header and the value itself.
+
+## Adding columns
+
+To add column, use data table builder's `addColumn()` method:
+
+```php #12-14 src/DataTable/Type/ProductDataTableType.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 ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addColumn('id', NumberColumnType::class)
+ ->addColumn('name', TextColumnType::class)
+ ->addColumn('createdAt', DateTimeColumnType::class)
+ ;
+ }
+}
+```
+
+The same method can also be used on already created data tables:
+
+```php #17-19 src/Controller/ProductController.php
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Kreyu\Bundle\DataTableBundle\Column\Type\NumberColumnType;
+use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType;
+use Kreyu\Bundle\DataTableBundle\Column\Type\DateTimeColumnType;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function index()
+ {
+ $dataTable = $this->createDataTable(ProductDataTableType::class);
+
+ $dataTable
+ ->addColumn('id', NumberColumnType::class)
+ ->addColumn('name', TextColumnType::class)
+ ->addColumn('createdAt', DateTimeColumnType::class)
+ ;
+ }
+}
+```
+
+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 [built-in column types](../../reference/columns/types.md).
+
+## Removing columns
+
+To remove existing column, use the builder's `removeColumn()` method:
+
+```php #8 src/DataTable/Type/ProductDataTableType.php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder->removeColumn('id');
+ }
+}
+```
+
+The same method can also be used on already created data tables:
+
+```php #16 src/Controller/ProductController.php
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Kreyu\Bundle\DataTableBundle\Column\Type\NumberColumnType;
+use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType;
+use Kreyu\Bundle\DataTableBundle\Column\Type\DateTimeColumnType;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+
+class ProductController extends AbstractController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function index()
+ {
+ $dataTable = $this->createDataTable(ProductDataTableType::class);
+
+ $dataTable->removeColumn('id');
+ }
+}
+```
+
+Any attempt of removing the non-existent column will silently fail.
+
+## Retrieving columns
+
+To retrieve already defined global columns, use the builder's `getColumns()` or `getColumn()` method:
+
+```php # src/DataTable/Type/ProductDataTableType.php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ // retrieve all previously defined columns:
+ $columns = $builder->getColumns();
+
+ // or specific column:
+ $column = $builder->getColumn('id');
+
+ // or simply check whether the column is defined:
+ if ($builder->hasColumn('id')) {
+ // ...
+ }
+ }
+}
+```
+
+The same methods are accessible on already created data tables:
+
+```php # src/Controller/ProductController.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);
+
+ // retrieve all previously defined columns:
+ $columns = $dataTable->getColumns();
+
+ // or specific column:
+ $column = $dataTable->getColumn('id');
+
+ // or simply check whether the column is defined:
+ if ($dataTable->hasColumn('id')) {
+ // ...
+ }
+ }
+}
+```
+
+!!!warning Warning
+Any attempt of retrieving a non-existent column will result in an `OutOfBoundsException`.
+To check whether the global column of given name exists, use the `hasColumn()` method.
+!!!
+
+!!!danger Important
+Within the data table builder, the columns are still in their build state!
+Therefore, columns retrieved by the methods:
+
+- `DataTableBuilderInterface::getColumns()`
+- `DataTableBuilderInterface::getColumn(string $name)`
+
+...are instance of `ColumnBuilderInterface`, whereas methods:
+
+- `DataTableInterface::getColumns()`
+- `DataTableInterface::getColumn(string $name)`
+
+...return instances of `ColumnInterface` instead.
+!!!
diff --git a/docs/features/exporting.md b/docs/features/exporting.md
old mode 100644
new mode 100755
index 8972ade3..f48275ed
--- a/docs/features/exporting.md
+++ b/docs/features/exporting.md
@@ -10,11 +10,14 @@ The data tables can be _exported_, with use of the [exporters](../reference/expo
## Prerequisites
-The built-in exporter types require [PhpSpreadsheet](https://phpspreadsheet.readthedocs.io/en/latest/).
-This library is not included as a bundle dependency, therefore, make sure it is installed:
+To start with, you have to install integration of some exporter library.
+
+The recommended exporter library is [OpenSpout](https://github.com/openspout/openspout).
+
+You can install the integration with the following command:
```bash
-$ composer require phpoffice/phpspreadsheet
+$ composer require kreyu/data-table-open-spout-bundle
```
## Toggling the feature
@@ -133,8 +136,8 @@ To add exporter, use the builder's `addExporter()` method on the data table buil
```php # src/DataTable/Type/ProductDataTableType.php
use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
-use Kreyu\Bundle\DataTableBundle\Bridge\PhpSpreadsheet\Exporter\Type\CsvExporterType;
-use Kreyu\Bundle\DataTableBundle\Bridge\PhpSpreadsheet\Exporter\Type\XlsxExporterType;
+use Kreyu\Bundle\DataTableOpenSpoutBundle\Exporter\Type\CsvExporterType;
+use Kreyu\Bundle\DataTableOpenSpoutBundle\Exporter\Type\XlsxExporterType;
class ProductDataTableType extends AbstractDataTableType
{
@@ -156,39 +159,35 @@ The builder's `addExporter()` method accepts _three_ arguments:
For reference, see [built-in exporter types](../reference/exporters/types.md).
-## Adding multiple exporters of the same type
+## Configuring default export data
-Let's think of a scenario where the user wants to export the data table to CSV format,
-but there's a catch — it must be possible to export as either comma or semicolon separated file.
+The default export data, such as filename, exporter, strategy and a flag to include personalization,
+can be configured using the data table builder's `setDefaultExportData()` method:
```php # src/DataTable/Type/ProductDataTableType.php
-use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
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
- ->addExporter('csv_comma', CsvExporterType::class, [
- 'label' => 'CSV (separated by comma)',
- 'delimiter' => ',',
- ])
- ->addExporter('csv_semicolon', CsvExporterType::class, [
- 'label' => 'CSV (separated by semicolon)',
- 'delimiter' => ';',
- ])
- ->addExporter('xlsx', XlsxExporterType::class)
+ ->setDefaultExportData(ExportData::fromArray([
+ 'filename' => sprintf('products_%s', date('Y-m-d')),
+ 'exporter' => 'xlsx',
+ 'strategy' => ExportStrategy::IncludeAll,
+ 'include_personalization' => true,
+ ]))
;
}
}
```
-## Downloading the file
-
-To download an export file, use the `export()` method on the data table.
+## Handling the export form
-If you're using data tables in controllers, use it in combination with `isExporting()` method:
+In the controller, use the `isExporting()` method to make sure the request should be handled as an export:
```php #15-17 src/Controller/ProductController.php
use App\DataTable\Type\ProductDataTableType;
@@ -241,57 +240,102 @@ class ProductController extends AbstractController
}
```
-If the data table has no specified exporters, this will result in an exception:
+The export data such as filename, exporter, strategy and a flag to include personalization,
+can be included by passing it directly to the `export()` method:
-> Unable to create export data from data table without exporters
+```php #13-14,16 src/Controller/ProductController.php
+use App\DataTable\Type\ProductDataTableType;
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
-By default, the export will contain records from **all pages**.
-Also, if enabled, the personalization will be **included**.
-To change this behaviour, either configure the data table type's default export data:
+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);
+
+ // ...
+ }
+}
+```
+
+## Exporting optimization
+
+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 # src/DataTable/Type/ProductDataTableType.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;
-use Kreyu\Bundle\DataTableBundle\Exporter\ExportData;
-use Kreyu\Bundle\DataTableBundle\Exporter\ExportStrategy;
class ProductDataTableType extends AbstractDataTableType
{
public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
{
- $exporters = $builder->getExporters();
-
- $builder->setDefaultExportData(ExportData::fromArray([
- 'filename' => 'products',
- 'exporter' => $exporters[0],
- 'strategy' => ExportStrategy::INCLUDE_CURRENT_PAGE,
- 'include_personalization' => false,
- ]));
+ $builder->addEventListener(DataTableEvents::PRE_EXPORT, function (DataTableEvent $event) {
+ $event->getDataTable()->getQuery()->setHydrationMode(AbstractQuery::HYDRATE_ARRAY);
+ });
}
}
```
-or pass the export data directly to the `export()` method:
+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 #13-14,16 src/Controller/ProductController.php
-use App\DataTable\Type\ProductDataTableType;
-use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
-use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+```php # src/DataTable/Type/ProductDataTableType.php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Column\Type\NumberColumnType;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
-class ProductController extends AbstractController
+class ProductDataTableType extends AbstractDataTableType
{
- use DataTableFactoryAwareTrait;
-
- public function index()
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
{
- $dataTable = $this->createDataTable(ProductDataTableType::class);
+ $builder
+ ->addColumn('id', NumberColumnType::class, [
+ 'export' => [
+ 'property_path' => '[id]',
+ ],
+ ])
+ ;
+ }
+}
- $exportData = ExportData::fromDataTable($dataTable);
- $exportData->includePersonalization = false;
-
- $file = $dataTable->export($exportData);
+```
+
+## Events
+
+Following events are dispatched when [:icon-mark-github: DataTableInterface::export()](https://github.com/Kreyu/data-table-bundle/blob/main/src/DataTableInterface.php) is called:
+
+[:icon-mark-github: DataTableEvents::PRE_EXPORT](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php)
+: Dispatched before the exporter is called.
+ Can be used to modify the exporting data, e.g. to force an export strategy or change the filename.
+
+The listeners and subscribers will receive an instance of the [:icon-mark-github: DataTableExportEvent](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableExportEvent.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, then save it in the event
+ $event->setExportData($exportData);
}
}
-```
+```
\ No newline at end of file
diff --git a/docs/features/filtering.md b/docs/features/filtering.md
old mode 100644
new mode 100755
index b7759b36..e0f34482
--- a/docs/features/filtering.md
+++ b/docs/features/filtering.md
@@ -103,12 +103,15 @@ return static function (KreyuDataTableConfig $config) {
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,
) {
}
@@ -118,7 +121,7 @@ class ProductDataTableType extends AbstractDataTableType
$resolver->setDefaults([
'filtration_persistence_enabled' => true,
'filtration_persistence_adapter' => $this->persistenceAdapter,
- 'filtration_persistence_subject' => $this->persistenceSubjectProvider->provide(),
+ 'filtration_persistence_subject_provider' => $this->persistenceSubjectProvider,
]);
}
}
@@ -129,6 +132,7 @@ 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
@@ -136,7 +140,9 @@ 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,
) {
}
@@ -149,7 +155,7 @@ class ProductController extends AbstractController
options: [
'filtration_persistence_enabled' => true,
'filtration_persistence_adapter' => $this->persistenceAdapter,
- 'filtration_persistence_subject' => $this->persistenceSubjectProvider->provide(),
+ 'filtration_persistence_subject_provider' => $this->persistenceSubjectProvider,
],
);
}
@@ -219,13 +225,36 @@ Optionally, the filtration form can display the operator selector, letting the u
### **Default operator**
-By default, each filter defines an array of supported operators.
-Those operators are then available to select by the user in the form.
-If operator selector is not visible, then the **first choice** is used.
+The default operator can be configured using the `default_operator` option:
-In case of the string filter, the default operator is `EQUALS`, because it is first in the supported operators array,
-stored in the `operator_options.choices` option. To change the default operator to `CONTAINS`,
-set the `choices` option to an array containing it as the first entry:
+```php # src/DataTable/Type/ProductDataTableType.php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\NumericFilterType;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\StringFilterType;
+use Kreyu\Bundle\DataTableBundle\Filter\Operator;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addFilter('id', NumericFilterType::class)
+ ->addFilter('name', StringFilterType::class, [
+ 'default_operator' => Operator::Contains,
+ ])
+ ;
+ }
+}
+```
+
+If the operator **is** selectable by the user, the `default_operator` determines the initially selected operator.
+
+If the operator **is not** selectable by the user, the operator provided by this option will be used.
+
+### Displaying operator selector
+
+The operator can be selectable by the user by setting the `operator_selectable` option to `true`:
```php # src/DataTable/Type/ProductDataTableType.php
use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
@@ -240,10 +269,38 @@ class ProductDataTableType extends AbstractDataTableType
$builder
->addFilter('id', NumericFilterType::class)
->addFilter('name', StringFilterType::class, [
- 'operator_options' => [
- 'choices' => [
- Operator::CONTAINS,
- ],
+ 'operator_selectable' => true,
+ ])
+ ;
+ }
+}
+```
+
+Setting the `operator_selectable` to `false` (by default) changes the operator form type to `HiddenType`.
+Because of that, even if you provide a different type using the `operator_form_type` option, it will be ignored.
+
+### Restricting selectable operators
+
+The operators selectable by the user can be restricted by using the `supported_operators` option:
+
+```php # src/DataTable/Type/ProductDataTableType.php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\NumericFilterType;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\StringFilterType;
+use Kreyu\Bundle\DataTableBundle\Filter\Operator;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->addFilter('id', NumericFilterType::class)
+ ->addFilter('name', StringFilterType::class, [
+ 'operator_selectable' => true,
+ 'supported_operators' => [
+ Operator::Equals,
+ Operator::Contains,
],
])
;
@@ -251,15 +308,18 @@ class ProductDataTableType extends AbstractDataTableType
}
```
-### Displaying operator selector
+Remember that each filter can support a different set of operators internally!
+
+## Configuring form type
-By default, the operator selector is not visible, because the `operator_options.visible` equals `false`. To change that, set the option to `true`:
+The filter form type can be configured using the `form_type` and `form_options` options.
```php # src/DataTable/Type/ProductDataTableType.php
use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\NumericFilterType;
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\StringFilterType;
+use Symfony\Component\Form\Extension\Core\Type\SearchType;
class ProductDataTableType extends AbstractDataTableType
{
@@ -268,8 +328,11 @@ class ProductDataTableType extends AbstractDataTableType
$builder
->addFilter('id', NumericFilterType::class)
->addFilter('name', StringFilterType::class, [
- 'operator_options' => [
- 'visible' => true,
+ 'form_type' => SearchType::class,
+ 'form_options' => [
+ 'attr' => [
+ 'placeholder' => 'Name',
+ ],
],
])
;
@@ -277,14 +340,14 @@ class ProductDataTableType extends AbstractDataTableType
}
```
-Of course, it is possible to define both options at once, restricting operators visible to the user:
+Similar configuration can be applied to the operator form type, using the `operator_form_type` and `operator_form_options` options:
```php # src/DataTable/Type/ProductDataTableType.php
use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\NumericFilterType;
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\StringFilterType;
-use Kreyu\Bundle\DataTableBundle\Filter\Operator;
+use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
class ProductDataTableType extends AbstractDataTableType
{
@@ -293,11 +356,10 @@ class ProductDataTableType extends AbstractDataTableType
$builder
->addFilter('id', NumericFilterType::class)
->addFilter('name', StringFilterType::class, [
- 'operator_options' => [
- 'visible' => true,
- 'choices' => [
- Operator::CONTAINS,
- Operator::NOT_CONTAINS,
+ 'operator_form_type' => ChoiceType::class,
+ 'operator_form_options' => [
+ 'attr' => [
+ 'placeholder' => 'Operator',
],
],
])
@@ -306,6 +368,9 @@ class ProductDataTableType extends AbstractDataTableType
}
```
+Setting the `operator_selectable` to `false` (by default) changes the operator form type to `HiddenType`.
+Because of that, even if you provide a different type using the `operator_form_type` option, it will be ignored.
+
## Configuring default filtration
The default filtration data can be overridden using the data table builder's `setDefaultFiltrationData()` method:
@@ -322,7 +387,7 @@ class ProductDataTableType extends AbstractDataTableType
public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
{
$builder->setDefaultFiltrationData(new FiltrationData([
- 'id' => new FilterData(value: 1, operator: Operator::CONTAINS),
+ 'id' => new FilterData(value: 1, operator: Operator::Contains),
]));
// or by creating the filtration data from an array:
@@ -332,3 +397,17 @@ class ProductDataTableType extends AbstractDataTableType
}
}
```
+
+## Events
+
+The following events are dispatched when [:icon-mark-github: DataTableInterface::filter()](https://github.com/Kreyu/data-table-bundle/blob/main/src/DataTableInterface.php) is called:
+
+[:icon-mark-github: DataTableEvents::PRE_FILTER](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php)
+: Dispatched before the filtration data is applied to the query.
+ Can be used to modify the filtration data, e.g. to force filtration on some columns.
+
+[:icon-mark-github: DataTableEvents::POST_FILTER](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php)
+: 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.
+
+The listeners and subscribers will receive an instance of the [:icon-mark-github: DataTableFiltrationEvent](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableFiltrationEvent.php)
\ No newline at end of file
diff --git a/docs/features/global-search.md b/docs/features/global-search.md
old mode 100644
new mode 100755
index 616da81e..324cfc12
--- a/docs/features/global-search.md
+++ b/docs/features/global-search.md
@@ -12,68 +12,47 @@ Sometimes all the user needs is a single text input, to quickly search through m
To handle that, there's a built-in special filter, which allows doing exactly that.
The uniqueness of this filter shines in the way it is rendered - in the built-in themes, instead of showing up in the filter form, it gets displayed above, always visible, easily accessible.
-## Adding the search filter
+## Adding the search handler
-To start, define a filter of search type — its name doesn't really matter:
+To define a search handler, use the builder's `setSearchHandler()` method to provide a callable,
+which gets an instance of query, and a search string as its arguments:
```php # src/DataTable/Type/ProductDataTableType.php
use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
-
-class ProductDataTableType extends AbstractDataTableType
-{
- public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
- {
- // Columns and filters added before...
-
- $builder
- ->addFilter('search', SearchFilterType::class)
- ;
- }
-}
-```
-
-Trying to display the data table with this filter configuration will result in an error:
-
-> The required option "handler" is missing.
-
-This is because the search filter type requires a `handler` option, which contains all the logic required for the data table search capabilities.
-
-## Writing the search handler
-
-The option accepts a callable, which gets an instance of query, and a search string as its arguments:
-
-```php #12 src/DataTable/Type/ProductDataTableType.php
-use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
-use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQuery;
class ProductDataTableType extends AbstractDataTableType
{
public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
{
- // Columns and filters added before...
-
$builder
- ->addFilter('search', SearchFilterType::class, [
- 'handler' => $this->handleSearchFilter(...),
- ])
+ ->setSearchHandler($this->handleSearchFilter(...))
;
}
- /**
- * @param DoctrineOrmProxyQuery $query
- */
- private function handleSearchFilter(ProxyQueryInterface $query, string $search): void
+ private function handleSearchFilter(DoctrineOrmProxyQuery $query, string $search): void
{
+ $alias = current($query->getRootAliases());
+
+ // Remember to use parameters to prevent SQL Injection!
+ // To help with that, DoctrineOrmProxyQuery has a special method "getUniqueParameterId",
+ // that will generate a unique parameter name (inside its query context), handy!
+ $parameter = $query->getUniqueParameterId();
+
+ $query
+ ->andWhere($query->expr()->eq("$alias.type", ":$parameter"))
+ ->setParameter($parameter, $data->getValue())
+ ;
+
$criteria = $query->expr()->orX(
- $query->expr()->like('product.id', ':search'),
- $query->expr()->like('product.name', ':search'),
+ $query->expr()->like("$alias.id", ":$parameter"),
+ $query->expr()->like("$alias.name", ":$parameter"),
);
$query
->andWhere($criteria)
- ->setParameter('search', '%' . $search . '%')
+ ->setParameter($parameter, "%$search%")
;
}
}
@@ -82,3 +61,27 @@ class ProductDataTableType extends AbstractDataTableType
!!!
**Tip**: Move the search handler logic into repository to reduce the type class complexity.
!!!
+
+## Adding the search filter
+
+The global search requires the user to provide a search query.
+This is handled by the [SearchFilterType](../reference/filters/types/search.md), which simply renders the search input.
+To help with that process, if the search handler is defined, the search filter will be added automatically.
+
+This filter will be named `__search`, which can be referenced using the constant:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
+
+$filter = $builder->getFilter(DataTableBuilderInterface::SEARCH_FILTER_NAME);
+```
+
+This behavior can be disabled (or enabled back again) using the builder's method:
+
+```php
+$builder->setAutoAddingSearchFilter(false);
+```
+
+!!!
+**Tip**: Because the global search is treated as a regular filter, it supports the [filtering persistence](filtering.md#configuring-the-feature-persistence).
+!!!
diff --git a/docs/features/index.yml b/docs/features/index.yml
old mode 100644
new mode 100755
diff --git a/docs/features/pagination.md b/docs/features/pagination.md
old mode 100644
new mode 100755
index dc6682ab..cbeeaf9b
--- a/docs/features/pagination.md
+++ b/docs/features/pagination.md
@@ -1,10 +1,10 @@
---
-order: a
+order: d
---
# Pagination
-The data tables can be _paginated_, which is crucial when working with extensive data sources.
+The data tables can be _paginated_, which is crucial when working with large data sources.
## Toggling the feature
@@ -188,4 +188,34 @@ class ProductDataTableType extends AbstractDataTableType
]));
}
}
+```
+
+## Events
+
+The following events are dispatched when [:icon-mark-github: DataTableInterface::paginate()](https://github.com/Kreyu/data-table-bundle/blob/main/src/DataTableInterface.php) is called:
+
+[:icon-mark-github: DataTableEvents::PRE_PAGINATE](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php)
+: 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.
+
+[:icon-mark-github: DataTableEvents::POST_PAGINATE](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php)
+: 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.
+
+The listeners and subscribers will receive an instance of the [:icon-mark-github: 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);
+ }
+}
```
\ No newline at end of file
diff --git a/docs/features/persistence.md b/docs/features/persistence.md
old mode 100644
new mode 100755
index b5059703..c8ceba1c
--- a/docs/features/persistence.md
+++ b/docs/features/persistence.md
@@ -265,3 +265,78 @@ services:
tags:
- { name: 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, personalization):
+
++++ Globally (YAML)
+```yaml # config/packages/kreyu_data_table.yaml
+kreyu_data_table:
+ defaults:
+ personalization:
+ persistence_subject_provider: app.data_table.persistence.subject_provider.custom
+```
++++ Globally (PHP)
+```php # config/packages/kreyu_data_table.php
+use Symfony\Config\KreyuDataTableConfig;
+
+return static function (KreyuDataTableConfig $config) {
+ $defaults = $config->defaults();
+ $defaults->personalization()
+ ->persistenceSubjectProvider('app.data_table.persistence.subject_provider.custom')
+ ;
+};
+```
++++ For data table type
+```php # src/DataTable/Type/ProductDataTable.php
+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,
+ ]);
+ }
+}
+```
++++ For specific data table
+```php # src/Controller/ProductController.php
+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,
+ ],
+ );
+ }
+}
+```
++++
\ No newline at end of file
diff --git a/docs/features/personalization.md b/docs/features/personalization.md
old mode 100644
new mode 100755
index e98bbe10..9d0352bd
--- a/docs/features/personalization.md
+++ b/docs/features/personalization.md
@@ -8,10 +8,10 @@ order: d
The data tables can be _personalized_, which can be helpful when working with many columns, by giving the user ability to:
-- set the order of the columns;
+- set the priority (order) of the columns;
- show or hide specific columns;
-### Prerequisites
+## Prerequisites
To begin with, make sure the [Symfony UX integration is enabled](../installation.md#enable-the-symfony-ux-integration).
Then, enable the **personalization** controller:
@@ -30,7 +30,7 @@ Then, enable the **personalization** controller:
```
:::
-### Toggling the feature
+## Toggling the feature
By default, the personalization feature is **disabled** for every data table.
@@ -91,7 +91,7 @@ class ProductController extends AbstractController
```
+++
-### Configuring the feature persistence
+## Configuring the feature persistence
By default, the personalization feature [persistence](persistence.md) is **disabled** for every data table.
@@ -182,9 +182,12 @@ class ProductController extends AbstractController
```
+++
-## Configuring default pagination
+## Configuring default personalization
-The default personalization data can be overridden using the data table builder's `setDefaultPersonalizationData()` method:
+There are two ways to configure the default personalization data for the data table:
+
+- using the columns [`priority`](../reference/columns/types/column.md#priority), [`visible`](../reference/columns/types/column.md#visible) and [`personalizable`](../reference/columns/types/column.md#personalizable) options (recommended);
+- using the data table builder's `setDefaultPersonalizationData()` method;
```php # src/DataTable/Type/ProductDataTableType.php
use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
@@ -196,17 +199,61 @@ 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', order: 0, visible: false),
- new PersonalizationColumnData(name: 'name', order: 1, visible: true),
+ 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, order = 0, visible = false
- 'id' => ['visible' => false],
- 'name' => ['order' => 1, 'visible' => true],
+ // each entry default values: name = from key, priority = 0, visible = false
+ 'id' => ['priority' => -1],
+ 'name' => ['visible' => false],
]));
}
}
```
+
+## Events
+
+The following events are dispatched when [:icon-mark-github: DataTableInterface::personalize()](https://github.com/Kreyu/data-table-bundle/blob/main/src/DataTableInterface.php) is called:
+
+[:icon-mark-github: DataTableEvents::PRE_PERSONALIZE](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php)
+: 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.
+
+[:icon-mark-github: DataTableEvents::POST_PERSONALIZE](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php)
+: 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.
+
+The listeners and subscribers will receive an instance of the [:icon-mark-github: 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/features/proxy-queries.md b/docs/features/proxy-queries.md
old mode 100644
new mode 100755
diff --git a/docs/features/request-handlers.md b/docs/features/request-handlers.md
old mode 100644
new mode 100755
diff --git a/docs/features/sorting.md b/docs/features/sorting.md
old mode 100644
new mode 100755
index d4ebb22d..58f58cd9
--- a/docs/features/sorting.md
+++ b/docs/features/sorting.md
@@ -114,6 +114,27 @@ class ProductDataTableType extends AbstractDataTableType
}
```
+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 # src/DataTable/Type/ProductDataTableType.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)',
+ ])
+ ;
+ }
+}
+```
+
## Configuring the feature persistence
By default, the sorting feature [persistence](persistence.md) is **disabled** for every data table.
@@ -151,12 +172,15 @@ return static function (KreyuDataTableConfig $config) {
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,
) {
}
@@ -166,7 +190,7 @@ class ProductDataTableType extends AbstractDataTableType
$resolver->setDefaults([
'sorting_persistence_enabled' => true,
'sorting_persistence_adapter' => $this->persistenceAdapter,
- 'sorting_persistence_subject' => $this->persistenceSubjectProvider->provide(),
+ 'sorting_persistence_subject_provider' => $this->persistenceSubjectProvider,
]);
}
}
@@ -178,13 +202,16 @@ 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,
) {
}
@@ -197,7 +224,7 @@ class ProductController extends AbstractController
options: [
'sorting_persistence_enabled' => true,
'sorting_persistence_adapter' => $this->persistenceAdapter,
- 'sorting_persistence_subject' => $this->persistenceSubjectProvider->provide(),
+ 'sorting_persistence_subject_provider' => $this->persistenceSubjectProvider,
],
);
}
@@ -234,3 +261,33 @@ class ProductDataTableType extends AbstractDataTableType
!!! 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 [:icon-mark-github: DataTableInterface::sort()](https://github.com/Kreyu/data-table-bundle/blob/main/src/DataTableInterface.php) is called:
+
+[:icon-mark-github: DataTableEvents::PRE_SORT](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php)
+: 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.
+
+[:icon-mark-github: DataTableEvents::POST_SORT](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php)
+: 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.
+
+The listeners and subscribers will receive an instance of the [:icon-mark-github: DataTableSortingEvent](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableSortingEvent.php):
+
+```php
+use Kreyu\Bundle\DataTableBundle\Event\DataTableSortingEvent;
+
+class DataTableExportListener
+{
+ 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/features/symfony-ux-turbo.md b/docs/features/symfony-ux-turbo.md
old mode 100644
new mode 100755
index 1be3888f..e37af714
--- a/docs/features/symfony-ux-turbo.md
+++ b/docs/features/symfony-ux-turbo.md
@@ -17,7 +17,7 @@ The next step is... voilà! ✨ You don't have to configure anything extra, your
The magic comes from the [:icon-mark-github: 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:
-{%{
+{% raw %}
```twig # @KreyuDataTable/themes/base.html.twig
{% block kreyu_data_table %}
@@ -25,7 +25,7 @@ which wraps the whole table in the `` tag:
{% endblock %}
```
-}%}
+{% endraw %}
This ensures every data table is wrapped in its own frame, making them work asynchronously.
diff --git a/docs/features/theming.md b/docs/features/theming.md
old mode 100644
new mode 100755
diff --git a/docs/features/type-classes.md b/docs/features/type-classes.md
old mode 100644
new mode 100755
index 24c627fd..bbc4baf6
--- a/docs/features/type-classes.md
+++ b/docs/features/type-classes.md
@@ -21,16 +21,20 @@ Following parts of the bundle are defined using the type classes:
The type classes work as a blueprint that defines a configuration how its feature should work. They implement their own, feature-specific interface.
However, it is better to extend from the abstract classes, which already implement the interface and provide some utilities.
-| Component | Interface | Abstract class |
-|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
-| Data tables | [:icon-mark-github: DataTableTypeInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Type/DataTableTypeInterface.php) | [:icon-mark-github: AbstractDataTableType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Type/AbstractDataTableType.php) |
-| Columns | [:icon-mark-github: ColumnTypeInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/ColumnTypeInterface.php) | [:icon-mark-github: AbstractColumnType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/AbstractColumnType.php) |
-| Filters | [:icon-mark-github: FilterTypeInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/FilterTypeInterface.php) | [:icon-mark-github: AbstractFilterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/AbstractFilterType.php) |
-| Actions | [:icon-mark-github: ActionTypeInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/ActionTypeInterface.php) | [:icon-mark-github: AbstractActionType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/AbstractActionType.php) |
-| Exporters | [:icon-mark-github: ExporterTypeInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/ExporterTypeInterface.php) | [:icon-mark-github: AbstractExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/AbstractExporterType.php) |
+{.compact}
+
+| Component | Interface | Abstract class |
+|-------------|----------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
+| Data tables | [:icon-mark-github: DataTableTypeInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Type/DataTableTypeInterface.php) | [:icon-mark-github: AbstractDataTableType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Type/AbstractDataTableType.php) |
+| Columns | [:icon-mark-github: ColumnTypeInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/ColumnTypeInterface.php) | [:icon-mark-github: AbstractColumnType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/AbstractColumnType.php) |
+| Filters | [:icon-mark-github: FilterTypeInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/FilterTypeInterface.php) | [:icon-mark-github: AbstractFilterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/AbstractFilterType.php) |
+| Actions | [:icon-mark-github: ActionTypeInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/ActionTypeInterface.php) | [:icon-mark-github: AbstractActionType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/AbstractActionType.php) |
+| Exporters | [:icon-mark-github: ExporterTypeInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/ExporterTypeInterface.php) | [:icon-mark-github: AbstractExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/AbstractExporterType.php) |
The recommended namespaces to put the types are as follows:
+{.compact}
+
| Component | Namespace |
|-------------|-------------------------------|
| Data tables | `App\DataTable\Type` |
@@ -41,6 +45,8 @@ The recommended namespaces to put the types are as follows:
Every type in the bundle is registered as a [tagged service](https://symfony.com/doc/current/service_container/tags.html):
+{.compact}
+
| Component | Type tag |
|-------------|----------------------------------|
| Data tables | `kreyu_data_table.type` |
@@ -167,12 +173,15 @@ class UserController extends AbstractController
The type extensions allow to easily extend existing types. Those classes contain methods similar as their corresponding feature type classes. They implement their own, feature-specific interface. For easier usage, there's also an abstract classes, which already implements the interface and provides some utilities.
-| Component | Interface | Abstract class |
-|-------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| Data tables | [:icon-mark-github: DataTableTypeExtensionInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Extension/DataTableTypeExtensionInterface.php) | [:icon-mark-github: AbstractDataTableTypeExtension](https://github.com/Kreyu/data-table-bundle/blob/main/src/Type/AbstractDataTableExtensionType.php) |
-| Columns | [:icon-mark-github: ColumnTypeExtensionInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Extension/ColumnTypeExtensionInterface.php) | [:icon-mark-github: AbstractColumnTypeExtension](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/AbstractColumnExtensionType.php) |
-| Filters | [:icon-mark-github: FilterTypeExtensionInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Extension/FilterTypeExtensionInterface.php) | [:icon-mark-github: AbstractFilterTypeExtension](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/AbstractFilterExtensionType.php) |
-| Actions | [:icon-mark-github: ActionTypeExtensionInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Extension/ActionTypeExtensionInterface.php) | [:icon-mark-github: AbstractFilterTypeExtension](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/AbstractFilterExtensionType.php) |
+{.compact}
+
+| Component | Interface | Abstract class |
+|-------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Data tables | [:icon-mark-github: DataTableTypeExtensionInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Extension/DataTableTypeExtensionInterface.php) | [:icon-mark-github: AbstractDataTableTypeExtension](https://github.com/Kreyu/data-table-bundle/blob/main/src/Type/AbstractDataTableExtensionType.php) |
+| Columns | [:icon-mark-github: ColumnTypeExtensionInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Extension/ColumnTypeExtensionInterface.php) | [:icon-mark-github: AbstractColumnTypeExtension](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/AbstractColumnExtensionType.php) |
+| Filters | [:icon-mark-github: FilterTypeExtensionInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Extension/FilterTypeExtensionInterface.php) | [:icon-mark-github: AbstractFilterTypeExtension](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/AbstractFilterExtensionType.php) |
+| Actions | [:icon-mark-github: ActionTypeExtensionInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Extension/ActionTypeExtensionInterface.php) | [:icon-mark-github: AbstractActionTypeExtension](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/AbstractActionExtensionType.php) |
+| Exporters | [:icon-mark-github: ExporterTypeExtensionInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Extension/ExporterTypeExtensionInterface.php) | [:icon-mark-github: AbstractExporterTypeExtension](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/AbstractExporterExtensionType.php) |
### Setting the types to extend
@@ -210,24 +219,29 @@ class ColumnTypeExtension extends AbstractColumnTypeExtension
For reference, a list of each feature base type class:
-| Component | Base type class |
-|-------------|----------------------------------------------------------------------------------------------------------------------------------|
-| Data tables | [:icon-mark-github: DataTableType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Type/DataTableType.php) |
-| Columns | [:icon-mark-github: ColumnType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/ColumnType.php) |
-| Filters | [:icon-mark-github: FilterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/FilterType.php) |
-| Actions | [:icon-mark-github: ActionType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/ActionType.php) |
-| Exporters | [:icon-mark-github: ExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/ExporterType.php) |
+{.compact}
+
+| Component | Base type class |
+|-------------|----------------------------------------------------------------------------------------------------------------------------|
+| Data tables | [:icon-mark-github: DataTableType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Type/DataTableType.php) |
+| Columns | [:icon-mark-github: ColumnType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/ColumnType.php) |
+| Filters | [:icon-mark-github: FilterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/FilterType.php) |
+| Actions | [:icon-mark-github: ActionType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/ActionType.php) |
+| Exporters | [:icon-mark-github: ExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/ExporterType.php) |
### Setting the extension order
Every type extension in the bundle is registered as a [tagged service](https://symfony.com/doc/current/service_container/tags.html):
+{.compact}
+
| Component | Service tag |
|-------------|--------------------------------------------|
| Data tables | `kreyu_data_table.type_extension` |
| Columns | `kreyu_data_table.column.type_extension` |
| Filters | `kreyu_data_table.filter.type_extension` |
| Actions | `kreyu_data_table.action.type_extension` |
+| Exporters | `kreyu_data_table.exporter.type_extension` |
Tagged services [can be prioritized using the `priority` attribute](https://symfony.com/doc/current/service\_container/tags.html#tagged-services-with-priority) to define the order the extensions will be loaded:
@@ -242,7 +256,7 @@ services:
- { name: kreyu_data_table.type_extension, priority: 2 }
```
-In the example above, the `ExtensionB` will be applied before the `Extension A`, because it has higher priority.
+In the example above, the `ExtensionB` will be applied before the `ExtensionA`, because it has higher priority.
Without the priority specified, the extensions would be applied in the order they are registered.
## Resolving the types
@@ -253,13 +267,15 @@ has direct access to an instance of the parent type (also resolved), as well as
Each component that supports the type classes, contain its "resolved" counterpart:
-| Component | Resolved type class |
-|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
-| Data tables | [:icon-mark-github: ResolvedDataTableType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Type/ResolvedDataTableType.php) |
-| Columns | [:icon-mark-github: ResolvedColumnType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/ResolvedColumnType.php) |
-| Filters | [:icon-mark-github: ResolvedFilterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/ResolvedFilterType.php) |
-| Actions | [:icon-mark-github: ResolvedActionType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/ResolvedActionType.php) |
-| Exporters | [:icon-mark-github: ResolvedExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/ResolvedExporterType.php) |
+{.compact}
+
+| Component | Resolved type class |
+|-------------|--------------------------------------------------------------------------------------------------------------------------------------------|
+| Data tables | [:icon-mark-github: ResolvedDataTableType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Type/ResolvedDataTableType.php) |
+| Columns | [:icon-mark-github: ResolvedColumnType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/ResolvedColumnType.php) |
+| Filters | [:icon-mark-github: ResolvedFilterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/ResolvedFilterType.php) |
+| Actions | [:icon-mark-github: ResolvedActionType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/ResolvedActionType.php) |
+| Exporters | [:icon-mark-github: ResolvedExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/ResolvedExporterType.php) |
Resolved type classes contain similar methods as a non-resolved types.
To understand how resolving process works, take a look at implementation of the resolved data table type's `buildDataTable()` method:
@@ -313,14 +329,16 @@ while only requiring a fully qualified class name of the desired type.
Each component that supports the type classes contains its own registry:
-| Component | Resolved type class |
-|-------------|-------------------------------------------------------------------------------------------------------------------------------------|
-| Data tables | [:icon-mark-github: DataTableRegistry](https://github.com/Kreyu/data-table-bundle/blob/main/src/DataTableRegistry.php) |
-| Columns | [:icon-mark-github: ColumnRegistry](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/ColumnRegistry.php) |
-| Filters | [:icon-mark-github: FilterRegistry](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/FilterRegistry.php) |
-| Actions | [:icon-mark-github: ActionRegistry](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/ActionRegistry.php) |
-| Exporters | [:icon-mark-github: ExporterRegistry](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/ExporterRegistry.php) |
+{.compact}
+
+| Component | Resolved type class |
+|-------------|-------------------------------------------------------------------------------------------------------------------------------|
+| Data tables | [:icon-mark-github: DataTableRegistry](https://github.com/Kreyu/data-table-bundle/blob/main/src/DataTableRegistry.php) |
+| Columns | [:icon-mark-github: ColumnRegistry](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/ColumnRegistry.php) |
+| Filters | [:icon-mark-github: FilterRegistry](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/FilterRegistry.php) |
+| Actions | [:icon-mark-github: ActionRegistry](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/ActionRegistry.php) |
+| Exporters | [:icon-mark-github: ExporterRegistry](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/ExporterRegistry.php) |
In reality, the purpose of the registry is to:
- hold instances of the registered types and extensions;
-- create [resolved types](#resolving-the-types) using the [ :icon-mark-github: ResolvedTypeFactoryInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/ExporterRegistry.php);
+- create [resolved types](#resolving-the-types) using the [ :icon-mark-github: ResolvedTypeFactoryInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/ExporterRegistry.php);
diff --git a/docs/index.md b/docs/index.md
old mode 100644
new mode 100755
diff --git a/docs/installation.md b/docs/installation.md
old mode 100644
new mode 100755
diff --git a/docs/reference/actions/index.yml b/docs/reference/actions/index.yml
old mode 100644
new mode 100755
diff --git a/docs/reference/actions/types.md b/docs/reference/actions/types.md
old mode 100644
new mode 100755
diff --git a/docs/reference/actions/types/_action_options.md b/docs/reference/actions/types/_action_options.md
old mode 100644
new mode 100755
index 7c75deb3..86958f4b
--- a/docs/reference/actions/types/_action_options.md
+++ b/docs/reference/actions/types/_action_options.md
@@ -1,6 +1,6 @@
### `label`
-- **type**: `string` or `Symfony\Component\Translation\TranslatableMessage`
+- **type**: `string` or `Symfony\Component\Translation\TranslatableInterface`
- **default**: the label is "guessed" from the action name
A label representing the action.
diff --git a/docs/reference/actions/types/action.md b/docs/reference/actions/types/action.md
old mode 100644
new mode 100755
index e51c97a6..43363046
--- a/docs/reference/actions/types/action.md
+++ b/docs/reference/actions/types/action.md
@@ -1,6 +1,8 @@
---
label: Action
order: z
+tags:
+ - actions
---
# Action type
diff --git a/docs/reference/actions/types/button.md b/docs/reference/actions/types/button.md
old mode 100644
new mode 100755
index 201a1d29..5e22aea8
--- a/docs/reference/actions/types/button.md
+++ b/docs/reference/actions/types/button.md
@@ -1,6 +1,8 @@
---
label: Button
order: b
+tags:
+ - actions
---
# Button action type
diff --git a/docs/reference/actions/types/form.md b/docs/reference/actions/types/form.md
old mode 100644
new mode 100755
index cd3fbe54..af2abd04
--- a/docs/reference/actions/types/form.md
+++ b/docs/reference/actions/types/form.md
@@ -1,6 +1,8 @@
---
label: Form
order: c
+tags:
+ - actions
---
# Form action type
diff --git a/docs/reference/actions/types/link.md b/docs/reference/actions/types/link.md
old mode 100644
new mode 100755
index 7c623115..4f286965
--- a/docs/reference/actions/types/link.md
+++ b/docs/reference/actions/types/link.md
@@ -1,6 +1,8 @@
---
label: Link
order: b
+tags:
+ - actions
---
# Link action type
diff --git a/docs/reference/columns/index.yml b/docs/reference/columns/index.yml
old mode 100644
new mode 100755
diff --git a/docs/reference/columns/types.md b/docs/reference/columns/types.md
old mode 100644
new mode 100755
diff --git a/docs/reference/columns/types/_column_options.md b/docs/reference/columns/types/_column_options.md
old mode 100644
new mode 100755
index 8728df34..e99a81d2
--- a/docs/reference/columns/types/_column_options.md
+++ b/docs/reference/columns/types/_column_options.md
@@ -1,6 +1,6 @@
### `label`
-- **type**: `null`, `string` or `Symfony\Component\Translation\TranslatableMessage`
+- **type**: `null`, `string` or `Symfony\Component\Translation\TranslatableInterface`
- **default**: {{ option_label_default_value ?? '`null` - the label is "guessed" from the column name' }}
Sets the label that will be used when rendering the column header.
@@ -173,3 +173,33 @@ $builder
])
;
```
+
+### `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](../../../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](../../../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](../../../features/personalization.md).
diff --git a/docs/reference/columns/types/actions.md b/docs/reference/columns/types/actions.md
old mode 100644
new mode 100755
index 4c9e35df..bb637d1b
--- a/docs/reference/columns/types/actions.md
+++ b/docs/reference/columns/types/actions.md
@@ -1,6 +1,8 @@
---
label: Actions
order: j
+tags:
+ - columns
---
# Actions column type
diff --git a/docs/reference/columns/types/boolean.md b/docs/reference/columns/types/boolean.md
old mode 100644
new mode 100755
index 538ba81b..5cee4bad
--- a/docs/reference/columns/types/boolean.md
+++ b/docs/reference/columns/types/boolean.md
@@ -1,6 +1,8 @@
---
label: Boolean
order: d
+tags:
+ - columns
---
# Boolean column type
@@ -17,14 +19,14 @@ The `BooleanColumnType` represents a column with value displayed as a "yes" or "
### `label_true`
-- **type**: `string` or `Symfony\Component\Translation\TranslatableMessage`
+- **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\TranslatableMessage`
+- **type**: `string` or `Symfony\Component\Translation\TranslatableInterface`
- **default**: `'No'`
Sets the value that will be displayed if row value is falsy.
diff --git a/docs/reference/columns/types/checkbox.md b/docs/reference/columns/types/checkbox.md
old mode 100644
new mode 100755
index 3a7a2a73..3849d798
--- a/docs/reference/columns/types/checkbox.md
+++ b/docs/reference/columns/types/checkbox.md
@@ -1,6 +1,8 @@
---
label: Checkbox
order: k
+tags:
+ - columns
---
# Checkbox column type
diff --git a/docs/reference/columns/types/collection.md b/docs/reference/columns/types/collection.md
old mode 100644
new mode 100755
index de0a3b2e..eb82aa69
--- a/docs/reference/columns/types/collection.md
+++ b/docs/reference/columns/types/collection.md
@@ -1,6 +1,8 @@
---
label: Collection
order: g
+tags:
+ - columns
---
# Collection column type
@@ -60,7 +62,7 @@ The options resolver normalizer ensures the `property_path` is always present in
### `separator`
- **type**: `null` or `string`
-- **default**: `','`
+- **default**: `', '`
Sets the value displayed between every item in the collection.
diff --git a/docs/reference/columns/types/column.md b/docs/reference/columns/types/column.md
old mode 100644
new mode 100755
index 1262f664..3087739a
--- a/docs/reference/columns/types/column.md
+++ b/docs/reference/columns/types/column.md
@@ -1,6 +1,8 @@
---
label: Column
order: z
+tags:
+ - columns
---
# Column type
diff --git a/docs/reference/columns/types/date-period.md b/docs/reference/columns/types/date-period.md
old mode 100644
new mode 100755
index 30baae5a..5c4b22b5
--- a/docs/reference/columns/types/date-period.md
+++ b/docs/reference/columns/types/date-period.md
@@ -1,6 +1,8 @@
---
label: DatePeriod
order: f
+tags:
+ - columns
---
# DatePeriod column type
diff --git a/docs/reference/columns/types/date-time.md b/docs/reference/columns/types/date-time.md
old mode 100644
new mode 100755
index 4d79e878..8af05801
--- a/docs/reference/columns/types/date-time.md
+++ b/docs/reference/columns/types/date-time.md
@@ -1,11 +1,13 @@
---
label: DateTime
order: e
+tags:
+ - columns
---
# DateTime column type
-The `DateTimeColumnType` represents a column with value displayed as a date (and with time by default).
+The `DateTimeColumnType` represents a column with value displayed as a date and time.
+-------------+---------------------------------------------------------------------+
| Parent type | [ColumnType](column)
@@ -22,13 +24,6 @@ The `DateTimeColumnType` represents a column with value displayed as a date (and
The format specifier is the same as supported by [date](https://www.php.net/date).
-### `format`
-
-- **type**: `null` or `string`
-- **default**: `null`
-
-Sets the timezone passed to the date formatter.
-
## Inherited options
{{ include '_column_options' }}
diff --git a/docs/reference/columns/types/date.md b/docs/reference/columns/types/date.md
new file mode 100644
index 00000000..77ecda35
--- /dev/null
+++ b/docs/reference/columns/types/date.md
@@ -0,0 +1,31 @@
+---
+label: Date
+order: e
+tags:
+ - columns
+---
+
+# Date column type
+
+The `DateColumnType` represents a column with value displayed as a date.
+
+This column type works exactly like `DateTimeColumnType`, but has a different default format.
+
++-------------+---------------------------------------------------------------------+
+| Parent type | [DateTimeType](date-time.md)
++-------------+---------------------------------------------------------------------+
+| Class | [:icon-mark-github: DateColumnType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/DateColumnType.php)
++-------------+---------------------------------------------------------------------+
+
+## Options
+
+### `format`
+
+- **type**: `string`
+- **default**: `'d.m.Y'`
+
+The format specifier is the same as supported by [date](https://www.php.net/date).
+
+## Inherited options
+
+{{ include '_column_options' }}
diff --git a/docs/reference/columns/types/form.md b/docs/reference/columns/types/form.md
old mode 100644
new mode 100755
index 9d157d57..cd802d1a
--- a/docs/reference/columns/types/form.md
+++ b/docs/reference/columns/types/form.md
@@ -1,6 +1,8 @@
---
label: Form
order: h
+tags:
+ - columns
---
# Form column type
diff --git a/docs/reference/columns/types/link.md b/docs/reference/columns/types/link.md
old mode 100644
new mode 100755
index 9ea81c18..9854a523
--- a/docs/reference/columns/types/link.md
+++ b/docs/reference/columns/types/link.md
@@ -1,6 +1,8 @@
---
label: Link
order: d
+tags:
+ - columns
---
# Link column type
diff --git a/docs/reference/columns/types/money.md b/docs/reference/columns/types/money.md
old mode 100644
new mode 100755
index dc2ee461..8f06de13
--- a/docs/reference/columns/types/money.md
+++ b/docs/reference/columns/types/money.md
@@ -1,6 +1,8 @@
---
label: Money
order: c
+tags:
+ - columns
---
# Money column type
diff --git a/docs/reference/columns/types/number.md b/docs/reference/columns/types/number.md
old mode 100644
new mode 100755
index 4db097f4..262dec28
--- a/docs/reference/columns/types/number.md
+++ b/docs/reference/columns/types/number.md
@@ -1,6 +1,8 @@
---
label: Number
order: b
+tags:
+ - columns
---
# Number column type
diff --git a/docs/reference/columns/types/template.md b/docs/reference/columns/types/template.md
old mode 100644
new mode 100755
index 85029ff2..e3293c69
--- a/docs/reference/columns/types/template.md
+++ b/docs/reference/columns/types/template.md
@@ -1,6 +1,8 @@
---
label: Template
order: i
+tags:
+ - columns
---
# Template column type
diff --git a/docs/reference/columns/types/text.md b/docs/reference/columns/types/text.md
old mode 100644
new mode 100755
index 6f7e8d68..22613648
--- a/docs/reference/columns/types/text.md
+++ b/docs/reference/columns/types/text.md
@@ -1,6 +1,8 @@
---
label: Text
order: a
+tags:
+ - columns
---
# Text column type
diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md
old mode 100644
new mode 100755
diff --git a/docs/reference/exporters/index.yml b/docs/reference/exporters/index.yml
old mode 100644
new mode 100755
diff --git a/docs/reference/exporters/types.md b/docs/reference/exporters/types.md
old mode 100644
new mode 100755
index 4d17703a..90a8fd78
--- a/docs/reference/exporters/types.md
+++ b/docs/reference/exporters/types.md
@@ -6,16 +6,16 @@ label: Available types
The following exporter types are natively available in the bundle:
+- [OpenSpout](https://github.com/openspout/openspout) (recommended)
+ - [Csv](types/open-spout/csv.md)
+ - [Xlsx](types/open-spout/xlsx.md)
+ - [Ods](types/open-spout/ods.md)
- [PhpSpreadsheet](https://github.com/PHPOffice/PhpSpreadsheet)
- - [Csv](types/php-spreadsheet/csv.md)
- - [Xls](types/php-spreadsheet/xls.md)
- - [Xlsx](types/php-spreadsheet/xlsx.md)
- - [Html](types/php-spreadsheet/html.md)
- - [Ods](types/php-spreadsheet/ods.md)
- - [PhpSpreadsheet](types/php-spreadsheet/php-spreadsheet.md)
-- [OpenSpout](https://github.com/openspout/openspout)
- - [Csv](types/open-spout/csv.md)
- - [Xlsx](types/open-spout/xlsx.md)
- - [Ods](types/open-spout/ods.md)
+ - [Csv](types/php-spreadsheet/csv.md)
+ - [Xls](types/php-spreadsheet/xls.md)
+ - [Xlsx](types/php-spreadsheet/xlsx.md)
+ - [Html](types/php-spreadsheet/html.md)
+ - [Ods](types/php-spreadsheet/ods.md)
+ - [PhpSpreadsheet](types/php-spreadsheet/php-spreadsheet.md)
- Base exporters
- - [Exporter](types/exporter.md)
\ No newline at end of file
+ - [Exporter](types/exporter.md)
diff --git a/docs/reference/exporters/types/_exporter_options.md b/docs/reference/exporters/types/_exporter_options.md
old mode 100644
new mode 100755
index d2444b0f..6ea654ca
--- a/docs/reference/exporters/types/_exporter_options.md
+++ b/docs/reference/exporters/types/_exporter_options.md
@@ -1,56 +1,27 @@
-### `label`
-
-- **type**: `string` or `Symfony\Component\Translation\TranslatableMessage`
-- **default**: the label is "guessed" from the filter name
-
-Sets the label that will be used when rendering the filter.
-
-### `label_translation_parameters`
+### `use_headers`
-- **type**: `array`
-- **default**: `[]`
+- **type**: `bool`
+- **default**: `true`
-Sets the parameters used when translating the `label` option.
+Determines whether the exporter should add headers to the output file.
-### `translation_domain`
-
-- **type**: `false` or `string`
-- **default**: the default `KreyuDataTable` is used
-
-Sets the translation domain used when translating the translatable filter values.
-Setting the option to `false` disables translation for the filter.
-
-### `query_path`
+### `label`
- **type**: `null` or `string`
-- **default**: `null` the query path is "guessed" from the filter name
+- **default**: `null` the label is "guessed" from the exporter name
-Sets the path used in the proxy query to perform the filtering on.
+Sets the label of the exporter, visible in the export action modal.
-### `field_type`
+### `tempnam_dir`
-- **type**: `string`
-- **default**: `'Symfony\Component\Form\Extension\Core\Type\TextType`
-
-This is the form type used to render the filter field.
-
-### `field_options`
-
-- **type**: `array`
-- **default**: `[]`
+- **type**: `string`
+- **default**: the value returned by the `sys_get_temp_dir()` function
-This is the array that's passed to the form type specified in the `field_type` option.
+Sets the directory used to store temporary file during the export process.
-### `operator_type`
+### `tempnam_prefix`
- **type**: `string`
-- **default**: `Kreyu\Bundle\DataTableBundle\Filter\Form\Type\OperatorType`
-
-This is the form type used to render the operator field.
-
-### `operator_options`
-
-- **type**: `array`
-- **default**: `[]`
+- **default**: `exporter_`
-This is the array that's passed to the form type specified in the `operator_type` option.
+Sets the prefix used to generate temporary file names during the export process.
\ No newline at end of file
diff --git a/docs/reference/exporters/types/exporter.md b/docs/reference/exporters/types/exporter.md
old mode 100644
new mode 100755
diff --git a/docs/reference/exporters/types/open-spout/_open-spout_options.md b/docs/reference/exporters/types/open-spout/_open-spout_options.md
new file mode 100644
index 00000000..4a1f7dfd
--- /dev/null
+++ b/docs/reference/exporters/types/open-spout/_open-spout_options.md
@@ -0,0 +1,101 @@
+### `header_row_style`
+
+- **type**: `null`, `callable` or `OpenSpout\Common\Entity\Style\Style`
+- **default**: `null`
+
+Represents a style object to apply to the header row.
+A callable can be used to dynamically apply styles based on the row:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Bridge\OpenSpout\Exporter\Type\XlsxExporterType;
+use Kreyu\Bundle\DataTableBundle\HeaderRowView;
+use OpenSpout\Common\Entity\Style\Style;
+
+$builder
+ ->addExporter('xlsx', XlsxExporterType::class, [
+ 'header_row_style' => function (HeaderRowView $view, array $options): Style {
+ return (new Style())->setFontBold();
+ },
+ ])
+;
+```
+
+### `value_row_style`
+
+- **type**: `null`, `callable` or `OpenSpout\Common\Entity\Style\Style`
+- **default**: `null`
+
+Represents a style object to apply to the value rows.
+A callable can be used to dynamically apply styles based on the row:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Bridge\OpenSpout\Exporter\Type\XlsxExporterType;
+use Kreyu\Bundle\DataTableBundle\ValueRowView;
+use OpenSpout\Common\Entity\Style\Color;
+use OpenSpout\Common\Entity\Style\Style;
+
+$builder
+ ->addExporter('xlsx', XlsxExporterType::class, [
+ 'value_row_style' => function (ValueRowView $view, array $options): Style {
+ $style = new Style();
+
+ if ($view->data->getQuantity() === 0) {
+ $style->setFontColor(Color::RED);
+ }
+
+ return $style;
+ },
+ ])
+;
+```
+
+### `header_cell_style`
+
+- **type**: `null`, `callable` or `OpenSpout\Common\Entity\Style\Style`
+- **default**: `null`
+
+Represents a style object to apply to the header cells.
+A callable can be used to dynamically apply styles based on the column:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Bridge\OpenSpout\Exporter\Type\XlsxExporterType;
+use Kreyu\Bundle\DataTableBundle\Column\ColumnHeaderView;
+use OpenSpout\Common\Entity\Style\Style;
+
+$builder
+ ->addExporter('xlsx', XlsxExporterType::class, [
+ 'header_cell_style' => function (ColumnHeaderView $view, array $options): Style {
+ return (new Style())->setFontBold();
+ },
+ ])
+;
+```
+
+### `value_cell_style`
+
+- **type**: `null`, `callable` or `OpenSpout\Common\Entity\Style\Style`
+- **default**: `null`
+
+Represents a style object to apply to the value cells.
+A callable can be used to dynamically apply styles based on the column:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Bridge\OpenSpout\Exporter\Type\XlsxExporterType;
+use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView;
+use OpenSpout\Common\Entity\Style\Color;
+use OpenSpout\Common\Entity\Style\Style;
+
+$builder
+ ->addExporter('xlsx', XlsxExporterType::class, [
+ 'value_cell_style' => function (ColumnValueView $view, array $options): Style {
+ $style = new Style();
+
+ if ($view->data->getQuantity() === 0) {
+ $style->setFontColor(Color::RED);
+ }
+
+ return $style;
+ },
+ ])
+;
+```
diff --git a/docs/reference/exporters/types/open-spout/csv.md b/docs/reference/exporters/types/open-spout/csv.md
old mode 100644
new mode 100755
index 2b4c80a7..64e2a716
--- a/docs/reference/exporters/types/open-spout/csv.md
+++ b/docs/reference/exporters/types/open-spout/csv.md
@@ -1,6 +1,9 @@
---
label: CSV
order: a
+tags:
+ - exporters
+ - openspout
---
# OpenSpout CSV exporter type
@@ -8,7 +11,7 @@ order: a
The `CsvExporterType` represents an exporter that uses an [OpenSpout](https://github.com/openspout/openspout) CSV writer.
+---------------------+--------------------------------------------------------------+
-| Parent type | [ExporterType](../exporter.md)
+| Parent type | [OpenSpoutExporterType](open-spout.md)
+---------------------+--------------------------------------------------------------+
| Class | [:icon-mark-github: CsvExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/OpenSpout/Exporter/Type/CsvExporterType.php)
+---------------------+--------------------------------------------------------------+
@@ -17,20 +20,33 @@ The `CsvExporterType` represents an exporter that uses an [OpenSpout](https://gi
### `field_delimiter`
-**type**: `string` **default**: `','`
+- **type**: `string`
+- **default**: `','`
-Represents a string that separates the CSV files values.
+Represents a string that separates the values.
### `field_enclosure`
-**type**: `string` **default**: `'"'`
+- **type**: `string`
+- **default**: `'"'`
-Represents a string that wraps all CSV fields.
+Represents a string that wraps the values.
### `should_add_bom`
-**type**: `bool` **default**: `true`
+- **type**: `bool`
+- **default**: `true`
+
+Determines whether a BOM character should be added at the beginning of the file.
### `flush_threshold`
-**type**: `int` **default**: `500`
+- **type**: `int`
+- **default**: `500`
+
+Represents a number of rows after which the output should be flushed to a file.
+
+## Inherited options
+
+{{ include '_open-spout_options.md' }}
+{{ include '../_exporter_options.md' }}
diff --git a/docs/reference/exporters/types/open-spout/index.yml b/docs/reference/exporters/types/open-spout/index.yml
old mode 100644
new mode 100755
diff --git a/docs/reference/exporters/types/open-spout/ods.md b/docs/reference/exporters/types/open-spout/ods.md
old mode 100644
new mode 100755
index 00f95c60..82196df9
--- a/docs/reference/exporters/types/open-spout/ods.md
+++ b/docs/reference/exporters/types/open-spout/ods.md
@@ -1,6 +1,9 @@
---
label: ODS
order: c
+tags:
+ - exporters
+ - openspout
---
# OpenSpout ODS exporter type
@@ -8,25 +11,43 @@ order: c
The `OdsExporterType` represents an exporter that uses an [OpenSpout](https://github.com/openspout/openspout) ODS writer.
+---------------------+--------------------------------------------------------------+
-| Parent type | [ExporterType](../exporter.md)
+| Parent type | [OpenSpoutExporterType](open-spout.md)
+---------------------+--------------------------------------------------------------+
-| Class | [:icon-mark-github: OdsExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/OpenSpout/Exporter/Type/OdsExporterType.php)
+| Class | [:icon-mark-github: OdsExporterType](https://github.com/Kreyu/data-table-open-spout-bundle/blob/main/src/Bridge/OpenSpout/Exporter/Type/OdsExporterType.php)
+---------------------+--------------------------------------------------------------+
## Options
### `default_row_style`
-**type**: `\OpenSpout\Common\Entity\Style\Style` **default**: object of class with default values
+- **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`
+- **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 `int` **default**: `null`
+- **type**: `null` or `float`
+- **default**: `null`
+
+Represents a width that will be applied to all columns by default.
### `default_row_height`
-**type**: `null` or `int` **default**: `null`
+- **type**: `null` or `float`
+- **default**: `null`
+
+Represents a height that will be applied to all rows by default.
+
+## Inherited options
+
+{{ include '_open-spout_options.md' }}
+{{ include '../_exporter_options.md' }}
diff --git a/docs/reference/exporters/types/open-spout/open-spout.md b/docs/reference/exporters/types/open-spout/open-spout.md
new file mode 100644
index 00000000..739ccb47
--- /dev/null
+++ b/docs/reference/exporters/types/open-spout/open-spout.md
@@ -0,0 +1,25 @@
+---
+label: CSV
+order: a
+tags:
+ - exporters
+ - openspout
+---
+
+# OpenSpout exporter type
+
+The `OpenSpoutExporterType` represents base exporter type used for all OpenSpout-based exporters.
+
++---------------------+--------------------------------------------------------------+
+| Parent type | [ExporterType](../exporter.md)
++---------------------+--------------------------------------------------------------+
+| Class | [:icon-mark-github: OpenSpoutExporterType](https://github.com/Kreyu/data-table-open-spout-bundle/blob/main/src/Bridge/OpenSpout/Exporter/Type/OpenSpoutExporterType.php)
++---------------------+--------------------------------------------------------------+
+
+## Options
+
+{{ include '_open-spout_options.md' }}
+
+## Inherited options
+
+{{ include '../_exporter_options.md' }}
diff --git a/docs/reference/exporters/types/open-spout/xlsx.md b/docs/reference/exporters/types/open-spout/xlsx.md
old mode 100644
new mode 100755
index b6eaf248..2ae6aea3
--- a/docs/reference/exporters/types/open-spout/xlsx.md
+++ b/docs/reference/exporters/types/open-spout/xlsx.md
@@ -1,6 +1,9 @@
---
label: XLSX
order: b
+tags:
+ - exporters
+ - openspout
---
# OpenSpout XLSX exporter type
@@ -8,25 +11,52 @@ order: b
The `XlsxExporterType` represents an exporter that uses an [OpenSpout](https://github.com/openspout/openspout) XLSX writer.
+---------------------+--------------------------------------------------------------+
-| Parent type | [ExporterType](../exporter.md)
+| Parent type | [OpenSpoutExporterType](open-spout.md)
+---------------------+--------------------------------------------------------------+
-| Class | [:icon-mark-github: XlsxExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/OpenSpout/Exporter/Type/XlsxExporterType.php)
+| Class | [:icon-mark-github: XlsxExporterType](https://github.com/Kreyu/data-table-open-spout-bundle/blob/main/src/Bridge/OpenSpout/Exporter/Type/XlsxExporterType.php)
+---------------------+--------------------------------------------------------------+
## Options
### `default_row_style`
-**type**: `\OpenSpout\Common\Entity\Style\Style` **default**: object of class with default values
+- **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`
+- **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 `int` **default**: `null`
+- **type**: `null` or `float`
+- **default**: `null`
+
+Represents a width that will be applied to all columns by default.
### `default_row_height`
-**type**: `null` or `int` **default**: `null`
+- **type**: `null` or `float`
+- **default**: `null`
+
+Represents a height that will be applied to all rows by default.
+
+## Inherited options
+
+{{ include '_open-spout_options.md' }}
+{{ include '../_exporter_options.md' }}
diff --git a/docs/reference/exporters/types/php-spreadsheet/_html_options.md b/docs/reference/exporters/types/php-spreadsheet/_html_options.md
old mode 100644
new mode 100755
diff --git a/docs/reference/exporters/types/php-spreadsheet/_php-spreadsheet_options.md b/docs/reference/exporters/types/php-spreadsheet/_php-spreadsheet_options.md
old mode 100644
new mode 100755
diff --git a/docs/reference/exporters/types/php-spreadsheet/csv.md b/docs/reference/exporters/types/php-spreadsheet/csv.md
old mode 100644
new mode 100755
index 39d1c278..1fed945f
--- a/docs/reference/exporters/types/php-spreadsheet/csv.md
+++ b/docs/reference/exporters/types/php-spreadsheet/csv.md
@@ -10,7 +10,7 @@ The `CsvExporterType` represents an exporter that uses a [PhpSpreadsheet CSV wri
+---------------------+--------------------------------------------------------------+
| Parent type | [PhpSpreadsheetType](php-spreadsheet.md)
+---------------------+--------------------------------------------------------------+
-| Class | [:icon-mark-github: CsvExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/CsvExporterType.php)
+| Class | [:icon-mark-github: CsvExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/CsvExporterType.php)
+---------------------+--------------------------------------------------------------+
## Options
diff --git a/docs/reference/exporters/types/php-spreadsheet/html.md b/docs/reference/exporters/types/php-spreadsheet/html.md
old mode 100644
new mode 100755
index c897b7fc..83a0090a
--- a/docs/reference/exporters/types/php-spreadsheet/html.md
+++ b/docs/reference/exporters/types/php-spreadsheet/html.md
@@ -10,7 +10,7 @@ The `HtmlExporterType` represents an exporter that uses a [PhpSpreadsheet Html w
+---------------------+--------------------------------------------------------------+
| Parent type | [PhpSpreadsheetType](php-spreadsheet.md)
+---------------------+--------------------------------------------------------------+
-| Class | [:icon-mark-github: HtmlExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/HtmlExporterType.php)
+| Class | [:icon-mark-github: HtmlExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/HtmlExporterType.php)
+---------------------+--------------------------------------------------------------+
## Options
diff --git a/docs/reference/exporters/types/php-spreadsheet/index.yml b/docs/reference/exporters/types/php-spreadsheet/index.yml
old mode 100644
new mode 100755
diff --git a/docs/reference/exporters/types/php-spreadsheet/ods.md b/docs/reference/exporters/types/php-spreadsheet/ods.md
old mode 100644
new mode 100755
index b124b95e..93d90efc
--- a/docs/reference/exporters/types/php-spreadsheet/ods.md
+++ b/docs/reference/exporters/types/php-spreadsheet/ods.md
@@ -10,7 +10,7 @@ The `OdsExporterType` represents an exporter that uses a [PhpSpreadsheet Ods wri
+---------------------+--------------------------------------------------------------+
| Parent type | [PhpSpreadsheetType](php-spreadsheet.md)
+---------------------+--------------------------------------------------------------+
-| Class | [:icon-mark-github: OdsExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/OdsExporterType.php)
+| Class | [:icon-mark-github: OdsExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/OdsExporterType.php)
+---------------------+--------------------------------------------------------------+
## Options
diff --git a/docs/reference/exporters/types/php-spreadsheet/php-spreadsheet.md b/docs/reference/exporters/types/php-spreadsheet/php-spreadsheet.md
old mode 100644
new mode 100755
index 89dd225d..d8146510
--- a/docs/reference/exporters/types/php-spreadsheet/php-spreadsheet.md
+++ b/docs/reference/exporters/types/php-spreadsheet/php-spreadsheet.md
@@ -10,7 +10,7 @@ The `PhpSpreadsheetExporterType` represents a base exporter, used as a parent fo
+---------------------+--------------------------------------------------------------+
| Parent type | [ExporterType](../exporter.md)
+---------------------+--------------------------------------------------------------+
-| Class | [:icon-mark-github: PhpSpreadsheetExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/PhpSpreadsheetExporterType.php)
+| Class | [:icon-mark-github: PhpSpreadsheetExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/PhpSpreadsheetExporterType.php)
+---------------------+--------------------------------------------------------------+
## Options
diff --git a/docs/reference/exporters/types/php-spreadsheet/xls.md b/docs/reference/exporters/types/php-spreadsheet/xls.md
old mode 100644
new mode 100755
index 2cead260..472716ca
--- a/docs/reference/exporters/types/php-spreadsheet/xls.md
+++ b/docs/reference/exporters/types/php-spreadsheet/xls.md
@@ -10,7 +10,7 @@ The `XlsExporterType` represents an exporter that uses a [PhpSpreadsheet Xls wri
+---------------------+--------------------------------------------------------------+
| Parent type | [PhpSpreadsheetType](php-spreadsheet.md)
+---------------------+--------------------------------------------------------------+
-| Class | [:icon-mark-github: XlsExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/XlsExporterType.php)
+| Class | [:icon-mark-github: XlsExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/XlsExporterType.php)
+---------------------+--------------------------------------------------------------+
## Options
diff --git a/docs/reference/exporters/types/php-spreadsheet/xlsx.md b/docs/reference/exporters/types/php-spreadsheet/xlsx.md
old mode 100644
new mode 100755
index 155d7eea..d420c060
--- a/docs/reference/exporters/types/php-spreadsheet/xlsx.md
+++ b/docs/reference/exporters/types/php-spreadsheet/xlsx.md
@@ -10,7 +10,7 @@ The `XlsxExporterType` represents an exporter that uses a [PhpSpreadsheet Xlsx w
+---------------------+--------------------------------------------------------------+
| Parent type | [PhpSpreadsheetType](php-spreadsheet.md)
+---------------------+--------------------------------------------------------------+
-| Class | [:icon-mark-github: XlsxExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/XlsxExporterType.php)
+| Class | [:icon-mark-github: XlsxExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/XlsxExporterType.php)
+---------------------+--------------------------------------------------------------+
## Options
diff --git a/docs/reference/filters/index.yml b/docs/reference/filters/index.yml
old mode 100644
new mode 100755
diff --git a/docs/reference/filters/types.md b/docs/reference/filters/types.md
old mode 100644
new mode 100755
index 93cbe3c4..20ebf442
--- a/docs/reference/filters/types.md
+++ b/docs/reference/filters/types.md
@@ -14,5 +14,7 @@ The following filter types are natively available in the bundle:
- [DateTime](types/doctrine-orm/date-time.md)
- [Entity](types/doctrine-orm/entity.md)
- [Callback](types/doctrine-orm/callback.md)
+- Special filters
+ - [Search](types/search.md)
- Base filters
- [Filter](types/filter.md)
\ No newline at end of file
diff --git a/docs/reference/filters/types/_filter_options.md b/docs/reference/filters/types/_filter_options.md
old mode 100644
new mode 100755
index d2444b0f..00cc4684
--- a/docs/reference/filters/types/_filter_options.md
+++ b/docs/reference/filters/types/_filter_options.md
@@ -1,7 +1,7 @@
### `label`
-- **type**: `string` or `Symfony\Component\Translation\TranslatableMessage`
-- **default**: the label is "guessed" from the filter name
+- **type**: `null`, `false`, `string` or `Symfony\Component\Translation\TranslatableInterface`
+- **default**: {{ option_label_default_value ?? '`null` - the label is "guessed" from the column name' }}
Sets the label that will be used when rendering the filter.
@@ -27,30 +27,78 @@ Setting the option to `false` disables translation for the filter.
Sets the path used in the proxy query to perform the filtering on.
-### `field_type`
+### `form_type`
- **type**: `string`
-- **default**: `'Symfony\Component\Form\Extension\Core\Type\TextType`
+- **default**: {{ option_form_type_default_value ?? '`\'Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType\'`' }}
-This is the form type used to render the filter field.
+This is the form type used to render the filter value field.
-### `field_options`
+### `form_options`
- **type**: `array`
-- **default**: `[]`
+- **default**: {{ option_form_options_default_value ?? '`[]`' }}
+
+This is the array that's passed to the form type specified in the `form_type` option.
+
+The normalizer ensures the default `['required' => false]` is added.
-This is the array that's passed to the form type specified in the `field_type` option.
+{{ option_form_options_notes }}
-### `operator_type`
+### `operator_form_type`
- **type**: `string`
- **default**: `Kreyu\Bundle\DataTableBundle\Filter\Form\Type\OperatorType`
-This is the form type used to render the operator field.
+This is the form type used to render the filter operator field.
-### `operator_options`
+!!!
+**Note**: if the `operator_selectable` option is `false`, the form type is changed to `Symfony\Component\Form\Extension\Core\Type\HiddenType` by the normalizer.
+!!!
+
+### `operator_form_options`
- **type**: `array`
- **default**: `[]`
-This is the array that's passed to the form type specified in the `operator_type` option.
+This is the array that's passed to the form type specified in the `operator_form_type` option.
+
+!!! Note
+The normalizer can change default value of this option based on another options:
+
+- if the `operator_selectable` option is `false`, the `default_operator` is used as a `data` option
+- if the `operator_form_type` is `OperatorType`, the `choices` array defaults to the `supported_operators` option
+- if the `operator_form_type` is `OperatorType`, the `empty_data` defaults to the `default_operator` option value.
+!!!
+
+### `default_operator`
+
+- **type**: `Kreyu\Bundle\DataTableBundle\Filter\Operator`
+- **default**: `Kreyu\Bundle\DataTableBundle\Filter\Operator\Operator::Equals`
+
+The default operator used for the filter.
+
+### `supported_operators`
+
+- **type**: `Kreyu\Bundle\DataTableBundle\Filter\Operator[]`
+- **default**: depends on the filters, see "supported operators" at the top of the page
+
+The operators supported by the filter.
+
+### `operator_selectable`
+
+- **type**: `bool`
+- **default**: `false`
+
+Determines whether the operator can be selected by the user.
+
+By setting this option to `false`, the normalizer changes the `operator_form_type` to `Symfony\Component\Form\Extension\Core\Type\HiddenType`.
+
+### `empty_data`
+
+- **type**: `string` or `array`
+- **default**: {{ option_empty_data_default_value ?? '`\'\'`' }}
+
+Represents a value of the filter when it's empty.
+
+{{ option_empty_data_note }}
diff --git a/docs/reference/filters/types/doctrine-orm/_doctrine_orm_filter_options.md b/docs/reference/filters/types/doctrine-orm/_doctrine_orm_filter_options.md
new file mode 100644
index 00000000..23aea655
--- /dev/null
+++ b/docs/reference/filters/types/doctrine-orm/_doctrine_orm_filter_options.md
@@ -0,0 +1,8 @@
+### `auto_alias_resolving`
+
+- **type**: `bool`
+- **default**: `true`
+
+Determines whether the root alias should be automatically resolved.
+This means that filtering on the `name` (no dot, therefore no alias e.g. `product.name`)
+the field will automatically resolve to `product.name` if the root alias is `product`.
diff --git a/docs/reference/filters/types/doctrine-orm/boolean.md b/docs/reference/filters/types/doctrine-orm/boolean.md
old mode 100644
new mode 100755
index f4cd155a..86b39844
--- a/docs/reference/filters/types/doctrine-orm/boolean.md
+++ b/docs/reference/filters/types/doctrine-orm/boolean.md
@@ -1,6 +1,9 @@
---
label: Boolean
order: c
+tags:
+ - filters
+ - doctrine orm
---
# Boolean filter type
@@ -8,13 +11,15 @@ order: c
The `BooleanFilterType` represents a filter that operates on boolean values.
+---------------------+--------------------------------------------------------------+
-| Parent type | [FilterType](../filter)
+| Parent type | [DoctrineOrmFilterType](doctrine-orm.md)
+---------------------+--------------------------------------------------------------+
| Class | [:icon-mark-github: BooleanFilterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/BooleanFilterType.php)
+---------------------+--------------------------------------------------------------+
| Form Type | [ChoiceType](https://symfony.com/doc/current/reference/forms/types/choice.html)
+---------------------+--------------------------------------------------------------+
-| Supported operators | EQUALS, NOT_EQUALS
+| Supported operators | Equals, NotEquals
++---------------------+--------------------------------------------------------------+
+| Default operator | Equals
+---------------------+--------------------------------------------------------------+
## Options
@@ -23,4 +28,7 @@ This filter type has no additional options.
## Inherited options
-{{ include '_filter_options' }}
+{{ option_form_type_default_value = '`\'Symfony\\Component\\Form\\Extension\\Core\\Type\\ChoiceType\'`' }}
+
+{{ include '../_filter_options' }}
+{{ include '_doctrine_orm_filter_options' }}
diff --git a/docs/reference/filters/types/doctrine-orm/callback.md b/docs/reference/filters/types/doctrine-orm/callback.md
old mode 100644
new mode 100755
index fd1821a6..980d409e
--- a/docs/reference/filters/types/doctrine-orm/callback.md
+++ b/docs/reference/filters/types/doctrine-orm/callback.md
@@ -1,6 +1,9 @@
---
label: Callback
order: g
+tags:
+ - filters
+ - doctrine orm
---
# Callback filter type
@@ -10,13 +13,15 @@ The `CallbackFilterType` represents a filter that operates on identifier values.
Displayed as a selector, allows the user to select a specific entity loaded from the database, to query by its identifier.
+---------------------+--------------------------------------------------------------+
-| Parent type | [FilterType](../../filter)
+| Parent type | [DoctrineOrmFilterType](doctrine-orm.md)
+---------------------+--------------------------------------------------------------+
| Class | [:icon-mark-github: CallbackFilterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/CallbackFilterType.php)
+---------------------+--------------------------------------------------------------+
| Form Type | [TextType](https://symfony.com/doc/current/reference/forms/types/text.html)
+---------------------+--------------------------------------------------------------+
-| Supported operators | Supports all operators, but it doesn't affect the actual query.
+| Supported operators | Supports all operators
++---------------------+--------------------------------------------------------------+
+| Default operator | Equals
+---------------------+--------------------------------------------------------------+
## Options
@@ -31,6 +36,7 @@ Sets callable that operates on the query passed as a first argument:
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\CallbackFilterType;
use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQuery;
$builder
->addFilter('type', CallbackFilterType::class, [
@@ -43,7 +49,7 @@ $builder
$parameter = $query->getUniqueParameterId();
$query
- ->andWhere($query->expr()->eq("$alias.type"), ":$parameter")
+ ->andWhere($query->expr()->eq("$alias.type", ":$parameter"))
->setParameter($parameter, $data->getValue())
;
}
@@ -52,4 +58,5 @@ $builder
## Inherited options
-{{ include '_filter_options' }}
+{{ include '../_filter_options' }}
+{{ include '_doctrine_orm_filter_options' }}
diff --git a/docs/reference/filters/types/doctrine-orm/date-time.md b/docs/reference/filters/types/doctrine-orm/date-time.md
old mode 100644
new mode 100755
index c20d6caf..454cf852
--- a/docs/reference/filters/types/doctrine-orm/date-time.md
+++ b/docs/reference/filters/types/doctrine-orm/date-time.md
@@ -1,6 +1,9 @@
---
label: DateTime
order: e
+tags:
+ - filters
+ - doctrine orm
---
# DateTime filter type
@@ -8,13 +11,15 @@ order: e
The `DateTimeFilterType` represents a filter that operates on datetime values.
+---------------------+--------------------------------------------------------------+
-| Parent type | [FilterType](../../filter)
+| Parent type | [DoctrineOrmFilterType](doctrine-orm.md)
+---------------------+--------------------------------------------------------------+
| Class | [:icon-mark-github: DateTimeFilterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/DateTimeFilterType.php)
+---------------------+--------------------------------------------------------------+
| Form Type | [DateTimeType](https://symfony.com/doc/current/reference/forms/types/datetime.html)
+---------------------+--------------------------------------------------------------+
-| Supported operators | EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_EQUALS, LESS_THAN, LESS_THAN_EQUALS
+| Supported operators | Equals, NotEquals, GreaterThan, GreaterThanEquals, LessThan, LessThanEquals
++---------------------+--------------------------------------------------------------+
+| Default operator | Equals
+---------------------+--------------------------------------------------------------+
## Options
@@ -23,4 +28,26 @@ This filter type has no additional options.
## Inherited options
-{{ include '_filter_options' }}
+{{ option_form_type_default_value = '`\'Symfony\\Component\\Form\\Extension\\Core\\Type\\DateTimeType\'`' }}
+
+{% capture option_empty_data_note %}
+If form option `widget` equals `'choice'` or `'text'` then the normalizer changes default value to:
+```
+[
+ 'date' => [
+ 'day' => '',
+ 'month' => '',
+ 'year' => ''
+ ]
+]
+```
+{% endcapture %}
+
+{% capture option_form_options_notes %}
+!!!
+**Note**: If the `form_type` is `DateTimeType`, the normalizer adds a default `['widget' => 'single_text']`.
+!!!
+{% endcapture %}
+
+{{ include '../_filter_options' }}
+{{ include '_doctrine_orm_filter_options' }}
\ No newline at end of file
diff --git a/docs/reference/filters/types/doctrine-orm/date.md b/docs/reference/filters/types/doctrine-orm/date.md
old mode 100644
new mode 100755
index e7deef53..dafcfcdd
--- a/docs/reference/filters/types/doctrine-orm/date.md
+++ b/docs/reference/filters/types/doctrine-orm/date.md
@@ -1,6 +1,9 @@
---
label: Date
order: d
+tags:
+ - filters
+ - doctrine orm
---
# Date filter type
@@ -8,13 +11,15 @@ order: d
The `DateFilterType` represents a filter that operates on date values.
+---------------------+--------------------------------------------------------------+
-| Parent type | [FilterType](../../filter)
+| Parent type | [DoctrineOrmFilterType](doctrine-orm.md)
+---------------------+--------------------------------------------------------------+
| Class | [:icon-mark-github: DateFilterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/DateFilterType.php)
+---------------------+--------------------------------------------------------------+
| Form Type | [DateType](https://symfony.com/doc/current/reference/forms/types/date.html)
+---------------------+--------------------------------------------------------------+
-| Supported operators | EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_EQUALS, LESS_THAN, LESS_THAN_EQUALS
+| Supported operators | Equals, NotEquals, GreaterThan, GreaterThanEquals, LessThan, LessThanEquals
++---------------------+--------------------------------------------------------------+
+| Default operator | Equals
+---------------------+--------------------------------------------------------------+
## Options
@@ -23,4 +28,18 @@ This filter type has no additional options.
## Inherited options
-{{ include '_filter_options' }}
+{{ option_form_type_default_value = '`\'Symfony\\Component\\Form\\Extension\\Core\\Type\\DateType\'`' }}
+
+{% capture option_empty_data_note %}
+If form option `widget` equals `'choice'` or `'text'` then the normalizer changes default value to:
+```
+[
+ 'day' => '',
+ 'month' => '',
+ 'year' => ''
+]
+```
+{% endcapture %}
+
+{{ include '../_filter_options' }}
+{{ include '_doctrine_orm_filter_options' }}
diff --git a/docs/reference/filters/types/doctrine-orm/doctrine-orm.md b/docs/reference/filters/types/doctrine-orm/doctrine-orm.md
new file mode 100644
index 00000000..33a8a033
--- /dev/null
+++ b/docs/reference/filters/types/doctrine-orm/doctrine-orm.md
@@ -0,0 +1,40 @@
+---
+label: DoctrineOrm
+order: z
+tags:
+ - filters
+ - doctrine orm
+---
+
+# Doctrine ORM filter type
+
+The `DoctrineOrmFilterType` represents a base filter used as a parent for every other Doctrine ORM filter type in the bundle.
+
++---------------------+--------------------------------------------------------------+
+| Parent type | [FilterType](../filter)
++---------------------+--------------------------------------------------------------+
+| Class | [:icon-mark-github: DoctrineOrmFilterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/Type/DoctrineOrmFilterType.php)
++---------------------+--------------------------------------------------------------+
+| Form Type | [TextType](https://symfony.com/doc/current/reference/forms/types/text.html)
++---------------------+--------------------------------------------------------------+
+
+## Options
+
+{{ include '_doctrine_orm_filter_options' }}
+
+## Inherited options
+
+{{ option_form_type_default_value = '`\'Symfony\\Component\\Form\\Extension\\Core\\Type\\DateType\'`' }}
+
+{% capture option_empty_data_note %}
+If form option `widget` equals `'choice'` or `'text'` then the normalizer changes default value to:
+```
+[
+ 'day' => '',
+ 'month' => '',
+ 'year' => ''
+]
+```
+{% endcapture %}
+
+{{ include '../_filter_options' }}
diff --git a/docs/reference/filters/types/doctrine-orm/entity.md b/docs/reference/filters/types/doctrine-orm/entity.md
old mode 100644
new mode 100755
index 5224600a..89542af2
--- a/docs/reference/filters/types/doctrine-orm/entity.md
+++ b/docs/reference/filters/types/doctrine-orm/entity.md
@@ -1,6 +1,9 @@
---
label: Entity
order: f
+tags:
+ - filters
+ - doctrine orm
---
# Entity filter type
@@ -10,13 +13,15 @@ The `EntityFilterType` represents a filter that operates on identifier values.
Displayed as a selector, allows the user to select a specific entity loaded from the database, to query by its identifier.
+---------------------+--------------------------------------------------------------+
-| Parent type | [FilterType](../../filter)
+| Parent type | [DoctrineOrmFilterType](doctrine-orm.md)
+---------------------+--------------------------------------------------------------+
| Class | [:icon-mark-github: EntityFilterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/EntityFilterType.php)
+---------------------+--------------------------------------------------------------+
| Form Type | [EntityType](https://symfony.com/doc/current/reference/forms/types/entity.html)
+---------------------+--------------------------------------------------------------+
-| Supported operators | EQUALS, NOT_EQUALS, CONTAINS, NOT_CONTAINS
+| Supported operators | Equals, NotEquals, Contains, NotContains
++---------------------+--------------------------------------------------------------+
+| Default operator | Equals
+---------------------+--------------------------------------------------------------+
## Options
@@ -25,4 +30,7 @@ This filter type has no additional options.
## Inherited options
-{{ include '_filter_options' }}
+{{ option_form_type_default_value = '`\'Symfony\\Bridge\\Doctrine\\Form\\Type\\EntityType\'`' }}
+
+{{ include '../_filter_options' }}
+{{ include '_doctrine_orm_filter_options' }}
\ No newline at end of file
diff --git a/docs/reference/filters/types/doctrine-orm/index.yml b/docs/reference/filters/types/doctrine-orm/index.yml
old mode 100644
new mode 100755
diff --git a/docs/reference/filters/types/doctrine-orm/numeric.md b/docs/reference/filters/types/doctrine-orm/numeric.md
old mode 100644
new mode 100755
index 167694d0..45101d8f
--- a/docs/reference/filters/types/doctrine-orm/numeric.md
+++ b/docs/reference/filters/types/doctrine-orm/numeric.md
@@ -1,21 +1,26 @@
---
label: Numeric
order: b
+tags:
+ - filters
+ - doctrine orm
---
# Numeric filter type
The `NumericFilterType` represents a filter that operates on numeric values.
-+---------------------+--------------------------------------------------------------------------------------+
-| Parent type | [FilterType](../filter)
-+---------------------+--------------------------------------------------------------------------------------+
++---------------------+--------------------------------------------------------------+
+| Parent type | [DoctrineOrmFilterType](doctrine-orm.md)
++---------------------+--------------------------------------------------------------+
| Class | [:icon-mark-github: NumericFilterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/NumericFilterType.php)
-+---------------------+--------------------------------------------------------------------------------------+
++---------------------+--------------------------------------------------------------+
| Form Type | [TextType](https://symfony.com/doc/current/reference/forms/types/text.html)
-+---------------------+--------------------------------------------------------------------------------------+
-| Supported operators | EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_EQUALS, LESS_THAN, LESS_THEN_EQUALS
-+---------------------+--------------------------------------------------------------------------------------+
++---------------------+--------------------------------------------------------------+
+| Supported operators | Equals, NotEquals, GreaterThan, GreaterThanEquals, LessThan, LessThanEquals
++---------------------+--------------------------------------------------------------+
+| Default operator | Equals
++---------------------+--------------------------------------------------------------+
## Options
@@ -23,4 +28,7 @@ This filter type has no additional options.
## Inherited options
-{{ include '_filter_options' }}
+{{ option_form_type_default_value = '`\'Symfony\\Component\\Form\\Extension\\Core\\Type\\NumberType\'`' }}
+
+{{ include '../_filter_options' }}
+{{ include '_doctrine_orm_filter_options' }}
diff --git a/docs/reference/filters/types/doctrine-orm/string.md b/docs/reference/filters/types/doctrine-orm/string.md
old mode 100644
new mode 100755
index 56e09137..bc2ffb06
--- a/docs/reference/filters/types/doctrine-orm/string.md
+++ b/docs/reference/filters/types/doctrine-orm/string.md
@@ -1,6 +1,9 @@
---
label: String
order: a
+tags:
+ - filters
+ - doctrine orm
---
# String filter type
@@ -8,13 +11,15 @@ order: a
The `StringFilterType` represents a filter that operates on string values.
+---------------------+--------------------------------------------------------------+
-| Parent type | [FilterType](../filter)
+| Parent type | [DoctrineOrmFilterType](doctrine-orm.md)
+---------------------+--------------------------------------------------------------+
| Class | [:icon-mark-github: StringFilterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/StringFilterType.php)
+---------------------+--------------------------------------------------------------+
| Form Type | [TextType](https://symfony.com/doc/current/reference/forms/types/text.html)
+---------------------+--------------------------------------------------------------+
-| Supported operators | EQUALS, NOT_EQUALS, CONTAINS, NOT_CONTAINS
+| Supported operators | Equals, NotEquals, Contains, NotContains, StartsWith, EndsWith
++---------------------+--------------------------------------------------------------+
+| Default operator | Contains
+---------------------+--------------------------------------------------------------+
## Options
@@ -23,4 +28,5 @@ This filter type has no additional options.
## Inherited options
-{{ include '_filter_options' }}
+{{ include '../_filter_options' }}
+{{ include '_doctrine_orm_filter_options' }}
diff --git a/docs/reference/filters/types/filter.md b/docs/reference/filters/types/filter.md
old mode 100644
new mode 100755
index a9c4ff64..d99a375a
--- a/docs/reference/filters/types/filter.md
+++ b/docs/reference/filters/types/filter.md
@@ -1,6 +1,8 @@
---
label: Filter
order: z
+tags:
+ - filters
---
# Filter type
diff --git a/docs/reference/filters/types/search.md b/docs/reference/filters/types/search.md
new file mode 100644
index 00000000..fa591e98
--- /dev/null
+++ b/docs/reference/filters/types/search.md
@@ -0,0 +1,71 @@
+---
+label: Search
+tags:
+ - filters
+---
+
+# Search filter type
+
+The `SearchFilterType` represents a special filter to handle the [global search](../../../features/global-search.md) feature.
+
++---------------------+--------------------------------------------------------------+
+| Parent type | [FilterType](../filter)
++---------------------+--------------------------------------------------------------+
+| Class | [:icon-mark-github: SearchFilterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/SearchFilterType.php)
++---------------------+--------------------------------------------------------------+
+| Form Type | [SearchType](https://symfony.com/doc/current/reference/forms/types/search.html)
++---------------------+--------------------------------------------------------------+
+| Supported operators | Supports all operators
++---------------------+--------------------------------------------------------------+
+
+## 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;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQuery;
+
+$builder
+ ->addFilter('search', SearchFilterType::class, [
+ 'handler' => function (DoctrineOrmProxyQuery $query, string $search): void {
+ $alias = current($query->getRootAliases());
+
+ // Remember to use parameters to prevent SQL Injection!
+ // To help with that, DoctrineOrmProxyQuery has a special method "getUniqueParameterId",
+ // that will generate a unique parameter name (inside its query context), handy!
+ $parameter = $query->getUniqueParameterId();
+
+ $query
+ ->andWhere($query->expr()->eq("$alias.type", ":$parameter"))
+ ->setParameter($parameter, $data->getValue())
+ ;
+
+ $criteria = $query->expr()->orX(
+ $query->expr()->like("$alias.id", ":$parameter"),
+ $query->expr()->like("$alias.name", ":$parameter"),
+ );
+
+ $query
+ ->andWhere($criteria)
+ ->setParameter($parameter, "%$search%")
+ ;
+ }
+ ])
+```
+
+## Inherited options
+
+{{ option_label_default_value = '`false`' }}
+{{ option_form_type_default_value = '`\'Symfony\\Component\\Form\\Extension\\Core\\Type\\SearchType\'`' }}
+
+{% capture option_form_options_notes %}
+The normalizer ensures the default `['attr' => ['placeholder' => 'Search...']]` is added.
+{% endcapture %}
+
+{{ include '_filter_options' }}
diff --git a/docs/reference/index.yml b/docs/reference/index.yml
old mode 100644
new mode 100755
diff --git a/docs/reference/twig.md b/docs/reference/twig.md
old mode 100644
new mode 100755
diff --git a/docs/retype.yml b/docs/retype.yml
old mode 100644
new mode 100755
index 57ca4296..d7ac5d32
--- a/docs/retype.yml
+++ b/docs/retype.yml
@@ -7,15 +7,23 @@ url: https://data-table-bundle.swroblewski.pl/
favicon: static/favicon.png
branding:
title: DataTableBundle
- label: Docs
+ label: v0.14
links:
- text: GitHub
link: https://github.com/Kreyu/data-table-bundle
icon: mark-github
+ - text: Packagist
+ link: https://packagist.org/packages/kreyu/data-table-bundle
+ icon: package
+ - text: Tags
+ link: /tags
+ icon: tag
edit:
repo: https://github.com/Kreyu/data-table-bundle/edit/
base: /docs
meta:
title: " - DataTableBundle"
+templating:
+ liquid: true
footer:
- copyright: "© Copyright {{ year }}. All rights reserved."
\ No newline at end of file
+ copyright: "© Copyright {{ year }}. All rights reserved."
diff --git a/docs/static/action_confirmation_modal.png b/docs/static/action_confirmation_modal.png
old mode 100644
new mode 100755
diff --git a/docs/static/batch_action.png b/docs/static/batch_action.png
old mode 100644
new mode 100755
diff --git a/docs/static/export_modal.png b/docs/static/export_modal.png
old mode 100644
new mode 100755
diff --git a/docs/static/favicon.png b/docs/static/favicon.png
old mode 100644
new mode 100755
diff --git a/docs/static/global_action.png b/docs/static/global_action.png
old mode 100644
new mode 100755
diff --git a/docs/static/global_search.png b/docs/static/global_search.png
old mode 100644
new mode 100755
diff --git a/docs/static/personalization_modal.png b/docs/static/personalization_modal.png
old mode 100644
new mode 100755
diff --git a/docs/static/row_actions.png b/docs/static/row_actions.png
old mode 100644
new mode 100755
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
old mode 100644
new mode 100755
diff --git a/docs/upgrade-guide/0.14.md b/docs/upgrade-guide/0.14.md
new file mode 100755
index 00000000..e6206752
--- /dev/null
+++ b/docs/upgrade-guide/0.14.md
@@ -0,0 +1,188 @@
+# 0.13.* -> 0.14
+
+This update contains a few _possible_ breaking changes, many new deprecations added and some old deprecations removed.
+
+## Breaking changes
+
+### Personalization based on column priority, not order
+
+Columns now contains priority - the higher priority, the earlier it will be rendered.
+This is opposite of previous "order" logic, where first item had "0" order, second "1", third "2", etc.
+If your application uses personalization persistence, make sure to clear its data, otherwise the order of the columns will be in reverse!
+
+### New column export views
+
+The data table type classes now contain a new `buildExportView()` method.
+
+The column type classes now contain new `buildExportHeaderView` and `buildExportValueView` methods.
+
+These methods are meant to be _especially_ lightweight, and used exclusively for exporting.
+
+This is a **breaking change** if your application uses custom export-specific logic
+in **any** data table type `buildView()` method or **any** column type `buildHeaderView()` or `buildValueView()` methods!
+
+### Column, filter and exporter builders
+
+Internally, the columns, filters and exporters are now utilizing the builder pattern similar to data tables and actions.
+If your application contains custom logic using internal bundle classes, you _may_ need to update it.
+
+## New deprecations
+
+### Deprecated filter form-related options
+
+The form-related filter type options are now deprecated:
+
+- `field_type` use `form_type` instead
+- `field_options` use `form_options` instead
+- `operator_type` use `operator_form_type` instead
+- `operator_options` use `operator_form_options` instead
+
+To select a default operator for the filter, instead of overwriting the `operator_options.choices` option,
+use the new `default_operator` option.
+
+To display operator selector to the user, instead of using the `operator_options.visible` option,
+use the new `operator_selectable` option.
+
+To limit operator selection for the filter, instead of using the `operator_option.choices` option,
+use the new `supported_operators` option.
+
+### Deprecated built-in PhpSpreadsheet & OpenSpout integrations
+
+The built-in integration with PhpSpreadsheet and OpenSpout is now deprecated.
+
+The PhpSpreadsheet integration will not be officially supported (for now).
+
+The OpenSpout integration is now extracted to a separate package - install [kreyu/data-table-open-spout-bundle](https://github.com/Kreyu/data-table-open-spout-bundle) instead.
+
+### Deprecated builders name and options setter methods
+
+The following builders now have `setName`, `setOptions` and `setOption` methods deprecated:
+
+- `DataTableBuilderInterface`
+- `ColumnBuilderInterface`
+- `FilterBuilderInterface`
+- `ActionBuilderInterface`
+- `ExporterBuilderInterface`
+
+If you need to change default name of the data table type, override its `getName()` method:
+
+```php
+use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
+
+class ProductDataTableType extends AbstractDataTableType
+{
+ public function getName(): string
+ {
+ return 'unique_products';
+ }
+}
+```
+
+If you need to change the name dynamically to display multiple data tables of the same type,
+use the factory `createNamed()` or `createNamedBuilder()` methods:
+
+```php
+use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
+
+class ProductController
+{
+ use DataTableFactoryAwareTrait;
+
+ public function index(): string
+ {
+ $availableProducts = $this->createNamedDataTable('available_products', ProductDataTableType::class);
+ $unavailableProducts = $this->createNamedDataTable('unavailable_products', ProductDataTableType::class);
+
+ // ...
+ }
+}
+```
+
+Same logic applies to every other feature (columns, filters, actions and exporters),
+although only data table factory is commonly used in the applications.
+
+### Deprecated default configuration extension
+
+The `Kreyu\Bundle\DataTableBundle\Extension\Core\DefaultConfigurationDataTableTypeExtension` is now deprecated.
+The default configuration is now applied by the base `Kreyu\Bundle\DataTableBundle\Type\DataTableType`.
+
+This extension is no longer registered in the container, however, if your application
+has it registered in the container (e.g. to change its priority), remove the definition.
+
+### Deprecated data table persistence subject options
+
+The following data table type options are deprecated:
+
+- `filtration_persistence_subject`
+- `sorting_persistence_subject`
+- `pagination_persistence_subject`
+- `personalization_persistence_subject`
+
+Instead, use the subject provider (that will provide a persistence subject) options:
+
+- `filtration_persistence_subject_provider`
+- `sorting_persistence_subject_provider`
+- `pagination_persistence_subject_provider`
+- `personalization_persistence_subject_provider`
+
+## Deprecated uppercase snake cased operator enum cases
+
+The `Kreyu\Bundle\DataTableBundle\Filter\Operator` enum has changed its cases:
+
+| Before | After |
+|-----------------------|---------------------|
+| `EQUALS` | `Equals` |
+| `CONTAINS` | `Contains` |
+| `NOT_CONTAINS` | `NotContains` |
+| `NOT_EQUALS` | `NotEquals` |
+| `GREATER_THAN` | `GreaterThan` |
+| `GREATER_THAN_EQUALS` | `GreaterThanEquals` |
+| `LESS_THAN_EQUALS` | `LessThanEquals` |
+| `LESS_THAN` | `LessThan` |
+| `START_WITH` | `StartsWith` |
+| `END_WITH` | `EndsWith` |
+
+Replace all occurrences of the old cases with the new ones.
+
+**Note**: the previous cases are marked as deprecated, and internally are converted
+to the new ones using the `getNonDeprecatedCase()` method to ease the transition process.
+
+Additionally, the translation keys of operator cases have changed:
+
+| Operator | Translation key before | Translation key after |
+|--------------------|------------------------|--------------------------|
+| `Equal` | `EQUALS` | `Equals` |
+| `Contain` | `CONTAINS` | `Contains` |
+| `NotContain` | `NOT_CONTAINS` | `Not contains` |
+| `NotEqual` | `NOT_EQUALS` | `Not equals` |
+| `GreaterThan` | `GREATER_THAN` | `Greater than` |
+| `GreaterThanEqual` | `GREATER_THAN_EQUALS` | `Greater than or equals` |
+| `LessThanEqual` | `LESS_THAN_EQUALS` | `Less than or equals` |
+| `LessThan` | `LESS_THAN` | `Less than` |
+| `StartWith` | `STARTS_WITH` | `Starts with` |
+| `EndWith` | `ENDS_WITH` | `Ends with` |
+
+The translation change ensures that applications without translator component can use the translation key as fallback.
+
+## Deprecated uppercase snake cased export strategy enum cases
+
+The `Kreyu\Bundle\DataTableBundle\Exporter\ExportStrategy` enum has changed its cases:
+
+| Before | After |
+|------------------------|----------------------|
+| `INCLUDE_ALL` | `IncludeAll` |
+| `INCLUDE_CURRENT_PAGE` | `IncludeCurrentPage` |
+
+Replace all occurrences of the old cases with the new ones.
+
+**Note**: the previous cases are marked as deprecated, and internally are converted
+to the new ones using the `getNonDeprecatedCase()` method to ease the transition process.
+
+Additionally, the translation keys of operator cases have changed:
+
+| Strategy | Translation key before | Translation key after |
+|----------------------|------------------------|------------------------|
+| `IncludeAll` | `INCLUDE_ALL` | `Include all` |
+| `IncludeCurrentPage` | `INCLUDE_CURRENT_PAGE` | `Include current page` |
+
+The translation change ensures that applications without translator component can use the translation key as fallback.
diff --git a/docs/upgrade-guide/index.yml b/docs/upgrade-guide/index.yml
new file mode 100755
index 00000000..4708564f
--- /dev/null
+++ b/docs/upgrade-guide/index.yml
@@ -0,0 +1 @@
+visibility: hidden
\ No newline at end of file
diff --git a/phpstan.neon b/phpstan.neon
old mode 100644
new mode 100755
diff --git a/src/AbstractDependencyInjectionExtension.php b/src/AbstractDependencyInjectionExtension.php
new file mode 100644
index 00000000..caaf5685
--- /dev/null
+++ b/src/AbstractDependencyInjectionExtension.php
@@ -0,0 +1,71 @@
+typeContainer->has($name)) {
+ throw new InvalidArgumentException(sprintf('The %s type "%s" is not registered in the service container.', $this->getErrorContextName(), $name));
+ }
+
+ return $this->typeContainer->get($name);
+ }
+
+ public function hasType(string $name): bool
+ {
+ return $this->typeContainer->has($name);
+ }
+
+ public function getTypeExtensions(string $name): array
+ {
+ $extensions = [];
+
+ if (isset($this->typeExtensionServices[$name])) {
+ foreach ($this->typeExtensionServices[$name] as $extension) {
+ $extensions[] = $extension;
+
+ $extendedTypes = [];
+ foreach ($extension::getExtendedTypes() as $extendedType) {
+ $extendedTypes[] = $extendedType;
+ }
+
+ // validate the result of getExtendedTypes() to ensure it is consistent with the service definition
+ if (!\in_array($name, $extendedTypes, true)) {
+ throw new InvalidArgumentException(sprintf('The extended %s type "%s" specified for the type extension class "%s" does not match any of the actual extended types (["%s"]).', $this->getErrorContextName(), $name, $extension::class, implode('", "', $extendedTypes)));
+ }
+ }
+ }
+
+ return $extensions;
+ }
+
+ public function hasTypeExtensions(string $name): bool
+ {
+ return isset($this->typeExtensionServices[$name]);
+ }
+
+ /**
+ * @return class-string
+ */
+ abstract protected function getTypeClass(): string;
+
+ abstract protected function getErrorContextName(): string;
+}
diff --git a/src/AbstractExtension.php b/src/AbstractExtension.php
new file mode 100644
index 00000000..af80b060
--- /dev/null
+++ b/src/AbstractExtension.php
@@ -0,0 +1,127 @@
+
+ */
+ private array $types = [];
+
+ /**
+ * @var array>
+ */
+ private array $typeExtensions = [];
+
+ public function hasType(string $name): bool
+ {
+ if (!isset($this->types)) {
+ $this->initTypes();
+ }
+
+ return isset($this->types[$name]);
+ }
+
+ public function getTypeExtensions(string $name): array
+ {
+ if (!isset($this->typeExtensions)) {
+ $this->initTypeExtensions();
+ }
+
+ return $this->typeExtensions[$name] ?? [];
+ }
+
+ public function hasTypeExtensions(string $name): bool
+ {
+ if (!isset($this->typeExtensions)) {
+ $this->initTypeExtensions();
+ }
+
+ return isset($this->typeExtensions[$name]) && \count($this->typeExtensions[$name]) > 0;
+ }
+
+ /**
+ * @return array
+ */
+ protected function loadTypes(): array
+ {
+ return [];
+ }
+
+ /**
+ * @return array
+ */
+ protected function loadTypeExtensions(): array
+ {
+ return [];
+ }
+
+ /**
+ * @return class-string
+ */
+ abstract protected function getTypeClass(): string;
+
+ /**
+ * @return class-string
+ */
+ abstract protected function getTypeExtensionClass(): string;
+
+ abstract protected function getErrorContextName(): string;
+
+ protected function doGetType(string $name)
+ {
+ if (!isset($this->types)) {
+ $this->initTypes();
+ }
+
+ if (!isset($this->types[$name])) {
+ throw new InvalidArgumentException(sprintf('The %s type "%s" cannot be loaded by this extension.', $this->getErrorContextName(), $name));
+ }
+
+ return $this->types[$name];
+ }
+
+ private function initTypes(): void
+ {
+ $this->types = [];
+
+ $typeClass = $this->getTypeClass();
+
+ foreach ($this->loadTypes() as $type) {
+ if (!$type instanceof $typeClass) {
+ throw new UnexpectedTypeException($type, $typeClass);
+ }
+
+ $this->types[$type::class] = $type;
+ }
+ }
+
+ private function initTypeExtensions(): void
+ {
+ $this->typeExtensions = [];
+
+ $typeExtensionClass = $this->getTypeExtensionClass();
+
+ foreach ($this->loadTypeExtensions() as $extension) {
+ if (!$extension instanceof $typeExtensionClass) {
+ throw new UnexpectedTypeException($extension, $typeExtensionClass);
+ }
+
+ foreach ($extension::getExtendedTypes() as $extendedType) {
+ $this->typeExtensions[$extendedType][] = $extension;
+ }
+ }
+ }
+}
diff --git a/src/AbstractRegistry.php b/src/AbstractRegistry.php
new file mode 100644
index 00000000..af34c0fc
--- /dev/null
+++ b/src/AbstractRegistry.php
@@ -0,0 +1,123 @@
+
+ */
+ private array $types = [];
+
+ /**
+ * @var array, bool>
+ */
+ private array $checkedTypes = [];
+
+ /**
+ * @param iterable $extensions
+ */
+ public function __construct(
+ private readonly iterable $extensions,
+ private readonly mixed $resolvedTypeFactory,
+ ) {
+ $extensionClass = $this->getExtensionClass();
+
+ foreach ($extensions as $extension) {
+ if (!$extension instanceof $extensionClass) {
+ throw new UnexpectedTypeException($extension, $extensionClass);
+ }
+ }
+ }
+
+ /**
+ * @return TResolvedType
+ */
+ protected function doGetType(string $name)
+ {
+ if (!isset($this->types[$name])) {
+ $type = null;
+
+ foreach ($this->extensions as $extension) {
+ if ($extension->hasType($name)) {
+ $type = $extension->getType($name);
+ break;
+ }
+ }
+
+ if (!$type) {
+ $typeClass = $this->getTypeClass();
+
+ if (!class_exists($name)) {
+ throw new InvalidArgumentException(sprintf('Could not load %s type "%s": class does not exist.', $this->getErrorContextName(), $name));
+ }
+
+ if (!is_subclass_of($name, $typeClass)) {
+ throw new InvalidArgumentException(sprintf('Could not load %s type "%s": class does not implement "%s".', $this->getErrorContextName(), $name, $typeClass));
+ }
+
+ $type = new $name();
+ }
+
+ $this->types[$name] = $this->resolveType($type);
+ }
+
+ return $this->types[$name];
+ }
+
+ /**
+ * @return TResolvedType
+ */
+ private function resolveType($type)
+ {
+ $parentType = $type->getParent();
+ $fqcn = $type::class;
+
+ if (isset($this->checkedTypes[$fqcn])) {
+ $types = implode(' > ', array_merge(array_keys($this->checkedTypes), [$fqcn]));
+ throw new \LogicException(sprintf('Circular reference detected for %s type "%s" (%s).', $this->getErrorContextName(), $fqcn, $types));
+ }
+
+ $this->checkedTypes[$fqcn] = true;
+
+ $typeExtensions = [];
+
+ try {
+ foreach ($this->extensions as $extension) {
+ $typeExtensions[] = $extension->getTypeExtensions($fqcn);
+ }
+
+ return $this->resolvedTypeFactory->createResolvedType(
+ $type,
+ array_merge([], ...$typeExtensions),
+ $parentType ? $this->getType($parentType) : null,
+ );
+ } finally {
+ unset($this->checkedTypes[$fqcn]);
+ }
+ }
+
+ /**
+ * @return class-string
+ */
+ abstract protected function getTypeClass(): string;
+
+ /**
+ * @return class-string
+ */
+ abstract protected function getExtensionClass(): string;
+
+ abstract protected function getErrorContextName(): string;
+}
diff --git a/src/Action/Action.php b/src/Action/Action.php
old mode 100644
new mode 100755
diff --git a/src/Action/ActionBuilder.php b/src/Action/ActionBuilder.php
old mode 100644
new mode 100755
index f72067e6..b0e4a79b
--- a/src/Action/ActionBuilder.php
+++ b/src/Action/ActionBuilder.php
@@ -4,176 +4,21 @@
namespace Kreyu\Bundle\DataTableBundle\Action;
-use Kreyu\Bundle\DataTableBundle\Action\Type\ResolvedActionTypeInterface;
use Kreyu\Bundle\DataTableBundle\Exception\BadMethodCallException;
-class ActionBuilder implements ActionBuilderInterface
+class ActionBuilder extends ActionConfigBuilder implements ActionBuilderInterface
{
- private ActionContext $context = ActionContext::Global;
- private array $attributes = [];
- private bool $confirmable = false;
- private bool $locked = false;
-
- public function __construct(
- private string $name,
- private ResolvedActionTypeInterface $type,
- private array $options = [],
- ) {
- }
-
- public function getName(): string
- {
- return $this->name;
- }
-
- public function setName(string $name): static
- {
- if ($this->locked) {
- throw $this->createBuilderLockedException();
- }
-
- $this->name = $name;
-
- return $this;
- }
-
- public function getType(): ResolvedActionTypeInterface
- {
- return $this->type;
- }
-
- public function setType(ResolvedActionTypeInterface $type): static
- {
- if ($this->locked) {
- throw $this->createBuilderLockedException();
- }
-
- $this->type = $type;
-
- return $this;
- }
-
- public function getOptions(): array
- {
- return $this->options;
- }
-
- public function hasOption(string $name): bool
- {
- return array_key_exists($name, $this->options);
- }
-
- public function getOption(string $name, mixed $default = null): mixed
- {
- return $this->options[$name] ?? $default;
- }
-
- public function setOptions(array $options): static
- {
- if ($this->locked) {
- throw $this->createBuilderLockedException();
- }
-
- $this->options = $options;
-
- return $this;
- }
-
- public function setOption(string $name, mixed $value): static
- {
- if ($this->locked) {
- throw $this->createBuilderLockedException();
- }
-
- $this->options[$name] = $value;
-
- return $this;
- }
-
- public function getAttributes(): array
- {
- return $this->attributes;
- }
-
- public function hasAttribute(string $name): bool
- {
- return array_key_exists($name, $this->attributes);
- }
-
- public function getAttribute(string $name, mixed $default = null): mixed
- {
- return $this->attributes[$name] ?? $default;
- }
-
- public function setAttributes(array $attributes): static
- {
- if ($this->locked) {
- throw $this->createBuilderLockedException();
- }
-
- $this->attributes = $attributes;
-
- return $this;
- }
-
- public function setAttribute(string $name, mixed $value = null): static
- {
- if ($this->locked) {
- throw $this->createBuilderLockedException();
- }
-
- $this->attributes[$name] = $value;
-
- return $this;
- }
-
- public function getContext(): ActionContext
- {
- return $this->context;
- }
-
- public function setContext(ActionContext $context): static
- {
- if ($this->locked) {
- throw $this->createBuilderLockedException();
- }
-
- $this->context = $context;
-
- return $this;
- }
-
- public function isConfirmable(): bool
- {
- return $this->confirmable;
- }
-
- public function setConfirmable(bool $confirmable): static
+ public function getAction(): ActionInterface
{
if ($this->locked) {
throw $this->createBuilderLockedException();
}
- $this->confirmable = $confirmable;
-
- return $this;
- }
-
- public function getActionConfig(): ActionConfigInterface
- {
- $config = clone $this;
- $config->locked = true;
-
- return $config;
- }
-
- public function getAction(): ActionInterface
- {
return new Action($this->getActionConfig());
}
private function createBuilderLockedException(): BadMethodCallException
{
- return new BadMethodCallException('ActionConfigBuilder methods cannot be accessed anymore once the builder is turned into a ActionConfigInterface instance.');
+ return new BadMethodCallException('ActionBuilder methods cannot be accessed anymore once the builder is turned into a ActionConfigInterface instance.');
}
}
diff --git a/src/Action/ActionBuilderInterface.php b/src/Action/ActionBuilderInterface.php
old mode 100644
new mode 100755
diff --git a/src/Action/ActionConfigBuilder.php b/src/Action/ActionConfigBuilder.php
new file mode 100755
index 00000000..7fb4dd77
--- /dev/null
+++ b/src/Action/ActionConfigBuilder.php
@@ -0,0 +1,179 @@
+name;
+ }
+
+ public function setName(string $name): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->name = $name;
+
+ return $this;
+ }
+
+ public function getType(): ResolvedActionTypeInterface
+ {
+ return $this->type;
+ }
+
+ public function setType(ResolvedActionTypeInterface $type): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->type = $type;
+
+ return $this;
+ }
+
+ public function getOptions(): array
+ {
+ return $this->options;
+ }
+
+ public function hasOption(string $name): bool
+ {
+ return array_key_exists($name, $this->options);
+ }
+
+ public function getOption(string $name, mixed $default = null): mixed
+ {
+ return $this->options[$name] ?? $default;
+ }
+
+ public function setOptions(array $options): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->options = $options;
+
+ return $this;
+ }
+
+ public function setOption(string $name, mixed $value): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->options[$name] = $value;
+
+ return $this;
+ }
+
+ public function getAttributes(): array
+ {
+ return $this->attributes;
+ }
+
+ public function hasAttribute(string $name): bool
+ {
+ return array_key_exists($name, $this->attributes);
+ }
+
+ public function getAttribute(string $name, mixed $default = null): mixed
+ {
+ return $this->attributes[$name] ?? $default;
+ }
+
+ public function setAttributes(array $attributes): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->attributes = $attributes;
+
+ return $this;
+ }
+
+ public function setAttribute(string $name, mixed $value): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->attributes[$name] = $value;
+
+ return $this;
+ }
+
+ public function getContext(): ActionContext
+ {
+ return $this->context;
+ }
+
+ public function setContext(ActionContext $context): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->context = $context;
+
+ return $this;
+ }
+
+ public function isConfirmable(): bool
+ {
+ return $this->confirmable;
+ }
+
+ public function setConfirmable(bool $confirmable): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->confirmable = $confirmable;
+
+ return $this;
+ }
+
+ public function getActionConfig(): ActionConfigInterface
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $config = clone $this;
+ $config->locked = true;
+
+ return $config;
+ }
+
+ private function createBuilderLockedException(): BadMethodCallException
+ {
+ return new BadMethodCallException('ActionConfigBuilder methods cannot be accessed anymore once the builder is turned into a ActionConfigInterface instance.');
+ }
+}
diff --git a/src/Action/ActionConfigBuilderInterface.php b/src/Action/ActionConfigBuilderInterface.php
old mode 100644
new mode 100755
index 03b4aa23..6785914e
--- a/src/Action/ActionConfigBuilderInterface.php
+++ b/src/Action/ActionConfigBuilderInterface.php
@@ -8,17 +8,26 @@
interface ActionConfigBuilderInterface extends ActionConfigInterface
{
+ /**
+ * @deprecated since 0.14.0, provide the name using the factory {@see ActionFactoryInterface} "named" methods instead
+ */
public function setName(string $name): static;
public function setType(ResolvedActionTypeInterface $type): static;
+ /**
+ * @deprecated since 0.14.0, modifying the options dynamically will be removed as it creates unexpected behaviors
+ */
public function setOptions(array $options): static;
+ /**
+ * @deprecated since 0.14.0, modifying the options dynamically will be removed as it creates unexpected behaviors
+ */
public function setOption(string $name, mixed $value): static;
public function setAttributes(array $attributes): static;
- public function setAttribute(string $name, mixed $value = null): static;
+ public function setAttribute(string $name, mixed $value): static;
public function setContext(ActionContext $context): static;
diff --git a/src/Action/ActionConfigInterface.php b/src/Action/ActionConfigInterface.php
old mode 100644
new mode 100755
diff --git a/src/Action/ActionContext.php b/src/Action/ActionContext.php
old mode 100644
new mode 100755
diff --git a/src/Action/ActionFactory.php b/src/Action/ActionFactory.php
old mode 100644
new mode 100755
diff --git a/src/Action/ActionFactoryBuilder.php b/src/Action/ActionFactoryBuilder.php
new file mode 100644
index 00000000..5b1168ea
--- /dev/null
+++ b/src/Action/ActionFactoryBuilder.php
@@ -0,0 +1,88 @@
+resolvedTypeFactory = $resolvedTypeFactory;
+
+ return $this;
+ }
+
+ public function addExtension(ActionExtensionInterface $extension): static
+ {
+ $this->extensions[] = $extension;
+
+ return $this;
+ }
+
+ public function addExtensions(array $extensions): static
+ {
+ $this->extensions = array_merge($this->extensions, $extensions);
+
+ return $this;
+ }
+
+ public function addType(ActionTypeInterface $type): static
+ {
+ $this->types[] = $type;
+
+ return $this;
+ }
+
+ public function addTypes(array $types): static
+ {
+ foreach ($types as $type) {
+ $this->types[] = $type;
+ }
+
+ return $this;
+ }
+
+ public function addTypeExtension(ActionTypeExtensionInterface $typeExtension): static
+ {
+ foreach ($typeExtension::getExtendedTypes() as $extendedType) {
+ $this->typeExtensions[$extendedType][] = $typeExtension;
+ }
+
+ return $this;
+ }
+
+ public function addTypeExtensions(array $typeExtensions): static
+ {
+ foreach ($typeExtensions as $typeExtension) {
+ $this->addTypeExtension($typeExtension);
+ }
+
+ return $this;
+ }
+
+ public function getActionFactory(): ActionFactoryInterface
+ {
+ $extensions = $this->extensions;
+
+ if (\count($this->types) > 0 || \count($this->typeExtensions) > 0) {
+ $extensions[] = new PreloadedActionExtension($this->types, $this->typeExtensions);
+ }
+
+ $registry = new ActionRegistry($extensions, $this->resolvedTypeFactory ?? new ResolvedActionTypeFactory());
+
+ return new ActionFactory($registry);
+ }
+}
diff --git a/src/Action/ActionFactoryBuilderInterface.php b/src/Action/ActionFactoryBuilderInterface.php
new file mode 100644
index 00000000..ba8dd92d
--- /dev/null
+++ b/src/Action/ActionFactoryBuilderInterface.php
@@ -0,0 +1,38 @@
+ $extensions
+ */
+ public function addExtensions(array $extensions): static;
+
+ public function addType(ActionTypeInterface $type): static;
+
+ /**
+ * @param array $types
+ */
+ public function addTypes(array $types): static;
+
+ public function addTypeExtension(ActionTypeExtensionInterface $typeExtension): static;
+
+ /**
+ * @param array $typeExtensions
+ */
+ public function addTypeExtensions(array $typeExtensions): static;
+
+ public function getActionFactory(): ActionFactoryInterface;
+}
diff --git a/src/Action/ActionFactoryInterface.php b/src/Action/ActionFactoryInterface.php
old mode 100644
new mode 100755
diff --git a/src/Action/ActionInterface.php b/src/Action/ActionInterface.php
old mode 100644
new mode 100755
diff --git a/src/Action/ActionRegistry.php b/src/Action/ActionRegistry.php
old mode 100644
new mode 100755
index 425bb1dd..86e2ffe8
--- a/src/Action/ActionRegistry.php
+++ b/src/Action/ActionRegistry.php
@@ -4,110 +4,33 @@
namespace Kreyu\Bundle\DataTableBundle\Action;
-use Kreyu\Bundle\DataTableBundle\Action\Extension\ActionTypeExtensionInterface;
+use Kreyu\Bundle\DataTableBundle\AbstractRegistry;
+use Kreyu\Bundle\DataTableBundle\Action\Extension\ActionExtensionInterface;
use Kreyu\Bundle\DataTableBundle\Action\Type\ActionTypeInterface;
-use Kreyu\Bundle\DataTableBundle\Action\Type\ResolvedActionTypeFactoryInterface;
use Kreyu\Bundle\DataTableBundle\Action\Type\ResolvedActionTypeInterface;
-use Kreyu\Bundle\DataTableBundle\Exception\UnexpectedTypeException;
-class ActionRegistry implements ActionRegistryInterface
+/**
+ * @extends AbstractRegistry
+ */
+class ActionRegistry extends AbstractRegistry implements ActionRegistryInterface
{
- /**
- * @var array
- */
- private array $types = [];
-
- /**
- * @var array
- */
- private array $resolvedTypes = [];
-
- /**
- * @var array
- */
- private array $checkedTypes = [];
-
- /**
- * @var array
- */
- private array $typeExtensions = [];
-
- /**
- * @param iterable $types
- * @param iterable $typeExtensions
- */
- public function __construct(
- iterable $types,
- iterable $typeExtensions,
- private ResolvedActionTypeFactoryInterface $resolvedColumnTypeFactory,
- ) {
- foreach ($types as $type) {
- if (!$type instanceof ActionTypeInterface) {
- throw new UnexpectedTypeException($type, ActionTypeInterface::class);
- }
-
- $this->types[$type::class] = $type;
- }
-
- foreach ($typeExtensions as $typeExtension) {
- if (!$typeExtension instanceof ActionTypeExtensionInterface) {
- throw new UnexpectedTypeException($typeExtension, ActionTypeExtensionInterface::class);
- }
-
- $this->typeExtensions[$typeExtension::class] = $typeExtension;
- }
- }
-
public function getType(string $name): ResolvedActionTypeInterface
{
- if (!isset($this->resolvedTypes[$name])) {
- if (!isset($this->types[$name])) {
- throw new \InvalidArgumentException(sprintf('Could not load type "%s".', $name));
- }
-
- $this->resolvedTypes[$name] = $this->resolveType($this->types[$name]);
- }
-
- return $this->resolvedTypes[$name];
+ return $this->doGetType($name);
}
- private function resolveType(ActionTypeInterface $type): ResolvedActionTypeInterface
+ final protected function getErrorContextName(): string
{
- $fqcn = $type::class;
-
- if (isset($this->checkedTypes[$fqcn])) {
- $types = implode(' > ', array_merge(array_keys($this->checkedTypes), [$fqcn]));
- throw new \LogicException(sprintf('Circular reference detected for action type "%s" (%s).', $fqcn, $types));
- }
-
- $this->checkedTypes[$fqcn] = true;
-
- $typeExtensions = array_filter(
- $this->typeExtensions,
- fn (ActionTypeExtensionInterface $extension) => $this->isFqcnExtensionEligible($fqcn, $extension),
- );
-
- $parentType = $type->getParent();
-
- try {
- return $this->resolvedColumnTypeFactory->createResolvedType(
- $type,
- $typeExtensions,
- $parentType ? $this->getType($parentType) : null,
- );
- } finally {
- unset($this->checkedTypes[$fqcn]);
- }
+ return 'action';
}
- private function isFqcnExtensionEligible(string $fqcn, ActionTypeExtensionInterface $extension): bool
+ final protected function getTypeClass(): string
{
- $extendedTypes = $extension::getExtendedTypes();
-
- if ($extendedTypes instanceof \Traversable) {
- $extendedTypes = iterator_to_array($extendedTypes);
- }
+ return ActionTypeInterface::class;
+ }
- return in_array($fqcn, $extendedTypes);
+ final protected function getExtensionClass(): string
+ {
+ return ActionExtensionInterface::class;
}
}
diff --git a/src/Action/ActionRegistryInterface.php b/src/Action/ActionRegistryInterface.php
old mode 100644
new mode 100755
diff --git a/src/Action/ActionView.php b/src/Action/ActionView.php
old mode 100644
new mode 100755
diff --git a/src/Action/Extension/AbstractActionExtension.php b/src/Action/Extension/AbstractActionExtension.php
new file mode 100644
index 00000000..7161bef4
--- /dev/null
+++ b/src/Action/Extension/AbstractActionExtension.php
@@ -0,0 +1,34 @@
+
+ */
+abstract class AbstractActionExtension extends AbstractExtension implements ActionExtensionInterface
+{
+ public function getType(string $name): ActionTypeInterface
+ {
+ return $this->doGetType($name);
+ }
+
+ final protected function getErrorContextName(): string
+ {
+ return 'action';
+ }
+
+ final protected function getTypeClass(): string
+ {
+ return ActionTypeInterface::class;
+ }
+
+ final protected function getTypeExtensionClass(): string
+ {
+ return ActionTypeExtensionInterface::class;
+ }
+}
diff --git a/src/Action/Extension/AbstractActionTypeExtension.php b/src/Action/Extension/AbstractActionTypeExtension.php
old mode 100644
new mode 100755
diff --git a/src/Action/Extension/ActionExtensionInterface.php b/src/Action/Extension/ActionExtensionInterface.php
new file mode 100644
index 00000000..5ef6b9c0
--- /dev/null
+++ b/src/Action/Extension/ActionExtensionInterface.php
@@ -0,0 +1,18 @@
+doGetType($name);
+ }
+
+ protected function getTypeClass(): string
+ {
+ return ActionTypeInterface::class;
+ }
+
+ protected function getErrorContextName(): string
+ {
+ return 'filter';
+ }
+}
diff --git a/src/Action/Extension/PreloadedActionExtension.php b/src/Action/Extension/PreloadedActionExtension.php
new file mode 100644
index 00000000..a05dd1b9
--- /dev/null
+++ b/src/Action/Extension/PreloadedActionExtension.php
@@ -0,0 +1,30 @@
+ $types
+ * @param array> $typeExtensions
+ */
+ public function __construct(
+ private readonly array $types = [],
+ private readonly array $typeExtensions = [],
+ ) {
+ }
+
+ protected function loadTypes(): array
+ {
+ return $this->types;
+ }
+
+ protected function loadTypeExtensions(): array
+ {
+ return $this->typeExtensions;
+ }
+}
diff --git a/src/Action/Type/AbstractActionType.php b/src/Action/Type/AbstractActionType.php
old mode 100644
new mode 100755
diff --git a/src/Action/Type/ActionType.php b/src/Action/Type/ActionType.php
old mode 100644
new mode 100755
diff --git a/src/Action/Type/ActionTypeInterface.php b/src/Action/Type/ActionTypeInterface.php
old mode 100644
new mode 100755
diff --git a/src/Action/Type/ButtonActionType.php b/src/Action/Type/ButtonActionType.php
old mode 100644
new mode 100755
diff --git a/src/Action/Type/FormActionType.php b/src/Action/Type/FormActionType.php
old mode 100644
new mode 100755
diff --git a/src/Action/Type/LinkActionType.php b/src/Action/Type/LinkActionType.php
old mode 100644
new mode 100755
diff --git a/src/Action/Type/ResolvedActionType.php b/src/Action/Type/ResolvedActionType.php
old mode 100644
new mode 100755
index 7656749a..ef04c7c3
--- a/src/Action/Type/ResolvedActionType.php
+++ b/src/Action/Type/ResolvedActionType.php
@@ -67,7 +67,7 @@ public function getTypeExtensions(): array
/**
* @throws ExceptionInterface
*/
- public function createBuilder(ActionFactoryInterface $factory, string $name, array $options = []): ActionBuilderInterface
+ public function createBuilder(ActionFactoryInterface $factory, string $name, array $options): ActionBuilderInterface
{
try {
$options = $this->getOptionsResolver()->resolve($options);
diff --git a/src/Action/Type/ResolvedActionTypeFactory.php b/src/Action/Type/ResolvedActionTypeFactory.php
old mode 100644
new mode 100755
index de372c5a..07a23f45
--- a/src/Action/Type/ResolvedActionTypeFactory.php
+++ b/src/Action/Type/ResolvedActionTypeFactory.php
@@ -6,7 +6,7 @@
class ResolvedActionTypeFactory implements ResolvedActionTypeFactoryInterface
{
- public function createResolvedType(ActionTypeInterface $type, array $typeExtensions, ResolvedActionTypeInterface $parent = null): ResolvedActionTypeInterface
+ public function createResolvedType(ActionTypeInterface $type, array $typeExtensions = [], ResolvedActionTypeInterface $parent = null): ResolvedActionTypeInterface
{
return new ResolvedActionType($type, $typeExtensions, $parent);
}
diff --git a/src/Action/Type/ResolvedActionTypeFactoryInterface.php b/src/Action/Type/ResolvedActionTypeFactoryInterface.php
old mode 100644
new mode 100755
index df05e7f7..fb6190a0
--- a/src/Action/Type/ResolvedActionTypeFactoryInterface.php
+++ b/src/Action/Type/ResolvedActionTypeFactoryInterface.php
@@ -4,7 +4,12 @@
namespace Kreyu\Bundle\DataTableBundle\Action\Type;
+use Kreyu\Bundle\DataTableBundle\Action\Extension\ActionTypeExtensionInterface;
+
interface ResolvedActionTypeFactoryInterface
{
- public function createResolvedType(ActionTypeInterface $type, array $typeExtensions, ResolvedActionTypeInterface $parent = null): ResolvedActionTypeInterface;
+ /**
+ * @param array $typeExtensions
+ */
+ public function createResolvedType(ActionTypeInterface $type, array $typeExtensions = [], ResolvedActionTypeInterface $parent = null): ResolvedActionTypeInterface;
}
diff --git a/src/Action/Type/ResolvedActionTypeInterface.php b/src/Action/Type/ResolvedActionTypeInterface.php
old mode 100644
new mode 100755
index 4389cd1a..bb07b8d9
--- a/src/Action/Type/ResolvedActionTypeInterface.php
+++ b/src/Action/Type/ResolvedActionTypeInterface.php
@@ -28,7 +28,7 @@ public function getInnerType(): ActionTypeInterface;
*/
public function getTypeExtensions(): array;
- public function createBuilder(ActionFactoryInterface $factory, string $name, array $options = []): ActionBuilderInterface;
+ public function createBuilder(ActionFactoryInterface $factory, string $name, array $options): ActionBuilderInterface;
public function createView(ActionInterface $action, DataTableView|ColumnValueView $parent): ActionView;
diff --git a/src/Bridge/Doctrine/Orm/Filter/Type/AbstractDoctrineOrmFilterType.php b/src/Bridge/Doctrine/Orm/Filter/Type/AbstractDoctrineOrmFilterType.php
new file mode 100644
index 00000000..c411b4ed
--- /dev/null
+++ b/src/Bridge/Doctrine/Orm/Filter/Type/AbstractDoctrineOrmFilterType.php
@@ -0,0 +1,96 @@
+
+ */
+abstract class AbstractDoctrineOrmFilterType extends AbstractFilterType
+{
+ public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter, array $options): void
+ {
+ if (!$query instanceof DoctrineOrmProxyQueryInterface) {
+ throw new UnexpectedTypeException($query, DoctrineOrmProxyQueryInterface::class);
+ }
+
+ $operator = $this->getFilterOperator($data, $filter);
+ $value = $this->getFilterValue($data);
+
+ if (!in_array($operator, $filter->getConfig()->getSupportedOperators())) {
+ return;
+ }
+
+ $queryPath = $this->getFilterQueryPath($query, $filter);
+
+ $parameterName = $this->getUniqueParameterName($query, $filter);
+
+ try {
+ $expression = $this->getOperatorExpression($queryPath, $parameterName, $operator, new Expr());
+ } catch (InvalidArgumentException) {
+ return;
+ }
+
+ $query
+ ->andWhere($expression)
+ ->setParameter($parameterName, $this->getParameterValue($operator, $value));
+ }
+
+ protected function getFilterOperator(FilterData $data, FilterInterface $filter): Operator
+ {
+ return $data->getOperator() ?? $filter->getConfig()->getDefaultOperator();
+ }
+
+ protected function getFilterValue(FilterData $data): mixed
+ {
+ return $data->getValue();
+ }
+
+ /**
+ * @throws InvalidArgumentException if operator is not supported by the filter
+ */
+ protected function getOperatorExpression(string $queryPath, string $parameterName, Operator $operator, Expr $expr): object
+ {
+ throw new InvalidArgumentException('Operator not supported');
+ }
+
+ public function getUniqueParameterName(DoctrineOrmProxyQueryInterface $query, FilterInterface $filter): string
+ {
+ return $filter->getFormName().'_'.$query->getUniqueParameterId();
+ }
+
+ protected function getParameterValue(Operator $operator, mixed $value): mixed
+ {
+ return $value;
+ }
+
+ protected function getFilterQueryPath(DoctrineOrmProxyQueryInterface $query, FilterInterface $filter): string
+ {
+ $rootAlias = current($query->getRootAliases());
+
+ $queryPath = $filter->getQueryPath();
+
+ if ($rootAlias && !str_contains($queryPath, '.') && $filter->getConfig()->getOption('auto_alias_resolving')) {
+ $queryPath = $rootAlias.'.'.$queryPath;
+ }
+
+ return $queryPath;
+ }
+
+ public function getParent(): ?string
+ {
+ return DoctrineOrmFilterType::class;
+ }
+}
diff --git a/src/Bridge/Doctrine/Orm/Filter/Type/AbstractFilterType.php b/src/Bridge/Doctrine/Orm/Filter/Type/AbstractFilterType.php
old mode 100644
new mode 100755
index b052ae3a..1d8d2499
--- a/src/Bridge/Doctrine/Orm/Filter/Type/AbstractFilterType.php
+++ b/src/Bridge/Doctrine/Orm/Filter/Type/AbstractFilterType.php
@@ -4,34 +4,9 @@
namespace Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type;
-use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQuery;
-use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
-use Kreyu\Bundle\DataTableBundle\Filter\Type\AbstractFilterType as BaseAbstractType;
-use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
-
-abstract class AbstractFilterType extends BaseAbstractType
+/**
+ * @deprecated since 0.14.0, use {@see AbstractDoctrineOrmFilterType} instead
+ */
+abstract class AbstractFilterType extends AbstractDoctrineOrmFilterType
{
- /**
- * @param DoctrineOrmProxyQuery $query
- */
- public function getUniqueParameterName(ProxyQueryInterface $query, FilterInterface $filter): string
- {
- return $filter->getFormName().'_'.$query->getUniqueParameterId();
- }
-
- /**
- * @param DoctrineOrmProxyQuery $query
- */
- protected function getFilterQueryPath(ProxyQueryInterface $query, FilterInterface $filter): string
- {
- $rootAlias = current($query->getRootAliases());
-
- $queryPath = $filter->getQueryPath();
-
- if ($rootAlias && !str_contains($queryPath, '.') && $filter->getOption('auto_alias_resolving')) {
- $queryPath = $rootAlias.'.'.$queryPath;
- }
-
- return $queryPath;
- }
}
diff --git a/src/Bridge/Doctrine/Orm/Filter/Type/BooleanFilterType.php b/src/Bridge/Doctrine/Orm/Filter/Type/BooleanFilterType.php
old mode 100644
new mode 100755
index a013de38..d76a34c8
--- a/src/Bridge/Doctrine/Orm/Filter/Type/BooleanFilterType.php
+++ b/src/Bridge/Doctrine/Orm/Filter/Type/BooleanFilterType.php
@@ -4,85 +4,51 @@
namespace Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type;
-use InvalidArgumentException;
-use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQuery;
+use Doctrine\ORM\Query\Expr;
+use Kreyu\Bundle\DataTableBundle\Exception\InvalidArgumentException;
use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
-use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
use Kreyu\Bundle\DataTableBundle\Filter\Operator;
-use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
-use Symfony\Component\Form\Extension\Core\Type as Form;
+use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
-use Symfony\Component\Translation\TranslatableMessage;
+use Symfony\Contracts\Translation\TranslatableInterface;
use function Symfony\Component\Translation\t;
-class BooleanFilterType extends AbstractFilterType
+class BooleanFilterType extends AbstractDoctrineOrmFilterType
{
- /**
- * @param DoctrineOrmProxyQuery $query
- */
- public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter, array $options): void
- {
- $operator = $data->getOperator() ?? Operator::EQUALS;
- $value = (bool) $data->getValue();
-
- try {
- $expressionBuilderMethodName = $this->getExpressionBuilderMethodName($operator);
- } catch (InvalidArgumentException) {
- return;
- }
-
- $parameterName = $this->getUniqueParameterName($query, $filter);
-
- $expression = $query->expr()->{$expressionBuilderMethodName}($this->getFilterQueryPath($query, $filter), ":$parameterName");
-
- $query
- ->andWhere($expression)
- ->setParameter($parameterName, $value);
- }
-
public function configureOptions(OptionsResolver $resolver): void
{
- $resolver->setDefault('field_type', Form\ChoiceType::class);
-
- $resolver->setNormalizer('field_options', function (Options $options, mixed $value) {
- if (!is_a($options['field_type'], Form\ChoiceType::class, true)) {
- return $value;
- }
-
- return $value + [
- 'choices' => [
- 'yes' => true,
- 'no' => false,
- ],
- 'choice_label' => function (bool $choice, string $key) {
- return t(ucfirst($key), [], 'KreyuDataTable');
+ $resolver
+ ->setDefaults([
+ 'form_type' => ChoiceType::class,
+ 'active_filter_formatter' => function (FilterData $data): TranslatableInterface {
+ return t($data->getValue() ? 'Yes' : 'No', domain: 'KreyuDataTable');
},
- ];
- });
-
- $resolver->setDefault('operator_options', function (OptionsResolver $resolver) {
- $resolver->setDefaults([
- 'visible' => false,
- 'choices' => [
- Operator::EQUALS,
- Operator::NOT_EQUALS,
- ],
- ]);
- });
-
- $resolver->setDefault('active_filter_formatter', function (FilterData $data, FilterInterface $filter, array $options): TranslatableMessage {
- return t($data->getValue() ? 'Yes' : 'No', [], 'KreyuDataTable');
- });
+ ])
+ ->addNormalizer('form_options', function (Options $options, mixed $value) {
+ if (ChoiceType::class !== $options['form_type']) {
+ return $value;
+ }
+
+ return $value + [
+ 'choices' => ['yes' => true, 'no' => false],
+ 'choice_label' => function (bool $choice, string $key): TranslatableInterface {
+ return t(ucfirst($key), domain: 'KreyuDataTable');
+ },
+ ];
+ })
+ ;
}
- private function getExpressionBuilderMethodName(Operator $operator): string
+ protected function getOperatorExpression(string $queryPath, string $parameterName, Operator $operator, Expr $expr): object
{
- return match ($operator) {
- Operator::EQUALS => 'eq',
- Operator::NOT_EQUALS => 'neq',
- default => throw new \InvalidArgumentException('Operator not supported'),
+ $expression = match ($operator) {
+ Operator::Equals => $expr->eq(...),
+ Operator::NotEquals => $expr->neq(...),
+ default => throw new InvalidArgumentException('Operator not supported'),
};
+
+ return $expression($queryPath, ":$parameterName");
}
}
diff --git a/src/Bridge/Doctrine/Orm/Filter/Type/CallbackFilterType.php b/src/Bridge/Doctrine/Orm/Filter/Type/CallbackFilterType.php
old mode 100644
new mode 100755
index 5f2b8ae0..f34a6d0b
--- a/src/Bridge/Doctrine/Orm/Filter/Type/CallbackFilterType.php
+++ b/src/Bridge/Doctrine/Orm/Filter/Type/CallbackFilterType.php
@@ -4,29 +4,29 @@
namespace Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type;
-use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQuery;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQueryInterface;
+use Kreyu\Bundle\DataTableBundle\Exception\UnexpectedTypeException;
use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
+use Kreyu\Bundle\DataTableBundle\Filter\Operator;
use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
-class CallbackFilterType extends AbstractFilterType
+class CallbackFilterType extends AbstractDoctrineOrmFilterType
{
- /**
- * @param DoctrineOrmProxyQuery $query
- */
public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter, array $options): void
{
+ if (!$query instanceof DoctrineOrmProxyQueryInterface) {
+ throw new UnexpectedTypeException($query, DoctrineOrmProxyQueryInterface::class);
+ }
+
$options['callback']($query, $data, $filter);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver
- ->setDefault('operator_options', [
- 'visible' => false,
- 'choices' => [],
- ])
+ ->setDefault('supported_operators', Operator::cases())
->setRequired('callback')
->setAllowedTypes('callback', ['callable'])
;
diff --git a/src/Bridge/Doctrine/Orm/Filter/Type/DateFilterType.php b/src/Bridge/Doctrine/Orm/Filter/Type/DateFilterType.php
old mode 100644
new mode 100755
index 39eca316..5bd1f1da
--- a/src/Bridge/Doctrine/Orm/Filter/Type/DateFilterType.php
+++ b/src/Bridge/Doctrine/Orm/Filter/Type/DateFilterType.php
@@ -4,82 +4,55 @@
namespace Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type;
-use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQuery;
+use Doctrine\ORM\Query\Expr;
+use Kreyu\Bundle\DataTableBundle\Exception\InvalidArgumentException;
use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
use Kreyu\Bundle\DataTableBundle\Filter\Operator;
-use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
-use Symfony\Component\Form\Extension\Core\Type as Form;
+use Symfony\Component\Form\Extension\Core\Type\DateType;
+use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
-class DateFilterType extends AbstractFilterType
+class DateFilterType extends AbstractDoctrineOrmFilterType
{
- /**
- * @param DoctrineOrmProxyQuery $query
- */
- public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter, array $options): void
- {
- $operator = $data->getOperator() ?? Operator::EQUALS;
- $value = $this->getDateTimeValue($data);
-
- try {
- $expressionBuilderMethodName = $this->getExpressionBuilderMethodName($operator);
- } catch (\InvalidArgumentException) {
- return;
- }
-
- $parameterName = $this->getUniqueParameterName($query, $filter);
-
- $expression = $query->expr()->{$expressionBuilderMethodName}($this->getFilterQueryPath($query, $filter), ":$parameterName");
-
- $query
- ->andWhere($expression)
- ->setParameter($parameterName, $value);
- }
-
public function configureOptions(OptionsResolver $resolver): void
{
- $resolver->setDefault('field_type', Form\DateType::class);
-
- $resolver->setDefault('operator_options', function (OptionsResolver $resolver) {
- $resolver->setDefaults([
- 'visible' => false,
- 'choices' => [
- Operator::EQUALS,
- Operator::NOT_EQUALS,
- Operator::GREATER_THAN,
- Operator::GREATER_THAN_EQUALS,
- Operator::LESS_THAN,
- Operator::LESS_THAN_EQUALS,
+ $resolver
+ ->setDefaults([
+ 'form_type' => DateType::class,
+ 'supported_operators' => [
+ Operator::Equals,
+ Operator::NotEquals,
+ Operator::GreaterThan,
+ Operator::GreaterThanEquals,
+ Operator::LessThan,
+ Operator::LessThanEquals,
],
- ]);
- });
-
- $resolver->setDefault('active_filter_formatter', function (FilterData $data, FilterInterface $filter, array $options): mixed {
- $value = $data->getValue();
-
- if ($value instanceof \DateTimeInterface) {
- return $value->format($options['field_options']['input_format'] ?? 'Y-m-d');
- }
-
- return $value;
- });
+ 'active_filter_formatter' => $this->getFormattedActiveFilterString(...),
+ ])
+ ->addNormalizer('form_options', function (Options $options, array $value): array {
+ if (DateType::class !== $options['form_type']) {
+ return $value;
+ }
+
+ return $value + ['widget' => 'single_text'];
+ })
+ ->addNormalizer('empty_data', function (Options $options, string|array $value): string|array {
+ if (DateType::class !== $options['form_type']) {
+ return $value;
+ }
+
+ // Note: because choice and text widgets are split into three fields,
+ // we have to return an array with three empty values to properly set the empty data.
+ return match ($options['form_options']['widget'] ?? null) {
+ 'choice', 'text' => ['day' => '', 'month' => '', 'year' => ''],
+ default => '',
+ };
+ })
+ ;
}
- private function getExpressionBuilderMethodName(Operator $operator): string
- {
- return match ($operator) {
- Operator::EQUALS => 'eq',
- Operator::NOT_EQUALS => 'neq',
- Operator::GREATER_THAN => 'gt',
- Operator::GREATER_THAN_EQUALS => 'gte',
- Operator::LESS_THAN => 'lt',
- Operator::LESS_THAN_EQUALS => 'lte',
- default => throw new \InvalidArgumentException('Operator not supported'),
- };
- }
-
- private function getDateTimeValue(FilterData $data): \DateTimeInterface
+ protected function getFilterValue(FilterData $data): \DateTimeInterface
{
$value = $data->getValue();
@@ -94,7 +67,7 @@ private function getDateTimeValue(FilterData $data): \DateTimeInterface
day: (int) $value['date']['day'] ?: 0,
);
} else {
- throw new \InvalidArgumentException(sprintf('Unable to convert data of type "%s" to DateTime object.', get_debug_type($value)));
+ throw new InvalidArgumentException(sprintf('Unable to convert data of type "%s" to DateTime object.', get_debug_type($value)));
}
$dateTime = \DateTime::createFromInterface($dateTime);
@@ -102,4 +75,30 @@ private function getDateTimeValue(FilterData $data): \DateTimeInterface
return $dateTime;
}
+
+ protected function getOperatorExpression(string $queryPath, string $parameterName, Operator $operator, Expr $expr): object
+ {
+ $expression = match ($operator) {
+ Operator::Equals => $expr->eq(...),
+ Operator::NotEquals => $expr->neq(...),
+ Operator::GreaterThan => $expr->gt(...),
+ Operator::GreaterThanEquals => $expr->gte(...),
+ Operator::LessThan => $expr->lt(...),
+ Operator::LessThanEquals => $expr->lte(...),
+ default => throw new InvalidArgumentException('Operator not supported'),
+ };
+
+ return $expression($queryPath, ":$parameterName");
+ }
+
+ private function getFormattedActiveFilterString(FilterData $data, FilterInterface $filter, array $options): string
+ {
+ $value = $data->getValue();
+
+ if ($value instanceof \DateTimeInterface) {
+ return $value->format($options['field_options']['input_format'] ?? 'Y-m-d');
+ }
+
+ return (string) $value;
+ }
}
diff --git a/src/Bridge/Doctrine/Orm/Filter/Type/DateRangeFilterType.php b/src/Bridge/Doctrine/Orm/Filter/Type/DateRangeFilterType.php
old mode 100644
new mode 100755
index 44d5e03c..2a49ae61
--- a/src/Bridge/Doctrine/Orm/Filter/Type/DateRangeFilterType.php
+++ b/src/Bridge/Doctrine/Orm/Filter/Type/DateRangeFilterType.php
@@ -4,7 +4,8 @@
namespace Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type;
-use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQuery;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQueryInterface;
+use Kreyu\Bundle\DataTableBundle\Exception\UnexpectedTypeException;
use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
use Kreyu\Bundle\DataTableBundle\Filter\Form\Type\DateRangeType;
@@ -14,13 +15,14 @@
use function Symfony\Component\Translation\t;
-class DateRangeFilterType extends AbstractFilterType
+class DateRangeFilterType extends AbstractDoctrineOrmFilterType
{
- /**
- * @param DoctrineOrmProxyQuery $query
- */
public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter, array $options): void
{
+ if (!$query instanceof DoctrineOrmProxyQueryInterface) {
+ throw new UnexpectedTypeException($query, DoctrineOrmProxyQueryInterface::class);
+ }
+
$value = $data->getValue();
$parameterName = $this->getUniqueParameterName($query, $filter);
@@ -29,7 +31,11 @@ public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterf
$criteria = $query->expr()->andX();
- if (null !== $dateFrom = $value['from']) {
+ if (!is_array($value)) {
+ return;
+ }
+
+ if (null !== $dateFrom = $value['from'] ?? null) {
$parameterNameFrom = $parameterName.'_from';
$dateFrom = \DateTime::createFromInterface($dateFrom);
@@ -40,7 +46,7 @@ public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterf
$query->setParameter($parameterNameFrom, $dateFrom);
}
- if (null !== $valueTo = $value['to']) {
+ if (null !== $valueTo = $value['to'] ?? null) {
$parameterNameTo = $parameterName.'_to';
$valueTo = \DateTime::createFromInterface($valueTo)->modify('+1 day');
@@ -51,15 +57,18 @@ public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterf
$query->setParameter($parameterNameTo, $valueTo);
}
- $query->andWhere($criteria);
+ if ($criteria->count() > 0) {
+ $query->andWhere($criteria);
+ }
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver
->setDefaults([
- 'field_type' => DateRangeType::class,
+ 'form_type' => DateRangeType::class,
'active_filter_formatter' => $this->getFormattedActiveFilterString(...),
+ 'empty_data' => ['from' => '', 'to' => ''],
])
;
}
diff --git a/src/Bridge/Doctrine/Orm/Filter/Type/DateTimeFilterType.php b/src/Bridge/Doctrine/Orm/Filter/Type/DateTimeFilterType.php
old mode 100644
new mode 100755
index 266f3d87..dc0d173f
--- a/src/Bridge/Doctrine/Orm/Filter/Type/DateTimeFilterType.php
+++ b/src/Bridge/Doctrine/Orm/Filter/Type/DateTimeFilterType.php
@@ -4,105 +4,70 @@
namespace Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type;
-use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQuery;
+use Doctrine\ORM\Query\Expr;
+use Kreyu\Bundle\DataTableBundle\Exception\InvalidArgumentException;
use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
use Kreyu\Bundle\DataTableBundle\Filter\Operator;
-use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
-use Symfony\Component\Form\Extension\Core\Type as Form;
+use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
+use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
-class DateTimeFilterType extends AbstractFilterType
+class DateTimeFilterType extends AbstractDoctrineOrmFilterType
{
- /**
- * @param DoctrineOrmProxyQuery $query
- */
- public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter, array $options): void
- {
- $operator = $data->getOperator() ?? Operator::EQUALS;
- $value = $this->getDateTimeValue($data);
-
- try {
- $expressionBuilderMethodName = $this->getExpressionBuilderMethodName($operator);
- } catch (\InvalidArgumentException) {
- return;
- }
-
- $parameterName = $this->getUniqueParameterName($query, $filter);
-
- $expression = $query->expr()->{$expressionBuilderMethodName}($this->getFilterQueryPath($query, $filter), ":$parameterName");
-
- $query
- ->andWhere($expression)
- ->setParameter($parameterName, $value);
- }
-
public function configureOptions(OptionsResolver $resolver): void
{
- $resolver->setDefault('field_type', Form\DateTimeType::class);
-
- $resolver->setDefault('operator_options', function (OptionsResolver $resolver) {
- $resolver->setDefaults([
- 'visible' => false,
- 'choices' => [
- Operator::EQUALS,
- Operator::NOT_EQUALS,
- Operator::GREATER_THAN,
- Operator::GREATER_THAN_EQUALS,
- Operator::LESS_THAN,
- Operator::LESS_THAN_EQUALS,
+ $resolver
+ ->setDefaults([
+ 'form_type' => DateTimeType::class,
+ 'supported_operators' => [
+ Operator::Equals,
+ Operator::NotEquals,
+ Operator::GreaterThan,
+ Operator::GreaterThanEquals,
+ Operator::LessThan,
+ Operator::LessThanEquals,
],
- ]);
- });
-
- $resolver->setDefault('active_filter_formatter', function (FilterData $data, FilterInterface $filter, array $options): mixed {
- $value = $data->getValue();
-
- if ($value instanceof \DateTimeInterface) {
- $format = $options['field_options']['input_format'] ?? null;
-
- if (null === $format) {
- $format = 'Y-m-d H';
-
- if ($options['field_options']['with_minutes'] ?? true) {
- $format .= ':i';
- }
-
- if ($options['field_options']['with_seconds'] ?? true) {
- $format .= ':s';
- }
+ 'active_filter_formatter' => $this->getFormattedActiveFilterString(...),
+ ])
+ ->addNormalizer('form_options', function (Options $options, array $value): array {
+ if (DateTimeType::class !== $options['form_type']) {
+ return $value;
}
- return $value->format($format);
- }
+ return $value + ['widget' => 'single_text'];
+ })
+ ->addNormalizer('empty_data', function (Options $options, string|array $value): string|array {
+ if (DateTimeType::class !== $options['form_type']) {
+ return $value;
+ }
- return $value;
- });
+ // Note: because choice and text widgets are split into three fields under "date" index,
+ // we have to return an array with three empty "date" values to properly set the empty data.
+ return match ($options['form_options']['widget'] ?? null) {
+ 'choice', 'text' => [
+ 'date' => ['day' => '', 'month' => '', 'year' => ''],
+ ],
+ default => '',
+ };
+ })
+ ;
}
- private function getExpressionBuilderMethodName(Operator $operator): string
- {
- return match ($operator) {
- Operator::EQUALS => 'eq',
- Operator::NOT_EQUALS => 'neq',
- Operator::GREATER_THAN => 'gt',
- Operator::GREATER_THAN_EQUALS => 'gte',
- Operator::LESS_THAN => 'lt',
- Operator::LESS_THAN_EQUALS => 'lte',
- default => throw new \InvalidArgumentException('Operator not supported'),
- };
- }
-
- private function getDateTimeValue(FilterData $data): \DateTimeInterface
+ protected function getFilterValue(FilterData $data): \DateTimeInterface
{
$value = $data->getValue();
if ($value instanceof \DateTimeInterface) {
- $dateTime = $value;
- } elseif (is_string($value)) {
- $dateTime = \DateTime::createFromFormat('Y-m-d\TH:i', $value);
- } elseif (is_array($value)) {
- $dateTime = (new \DateTime())
+ return $value;
+ }
+
+ if (is_string($value)) {
+ return \DateTime::createFromFormat('Y-m-d\TH:i', $value);
+ }
+
+ if (is_array($value)) {
+ return (new \DateTime())
->setDate(
year: (int) $value['date']['year'] ?: 0,
month: (int) $value['date']['month'] ?: 0,
@@ -114,10 +79,48 @@ private function getDateTimeValue(FilterData $data): \DateTimeInterface
second: (int) $value['time']['second'] ?: 0,
)
;
- } else {
- throw new \InvalidArgumentException(sprintf('Unable to convert data of type "%s" to DateTime object.', get_debug_type($value)));
}
- return $dateTime;
+ throw new \InvalidArgumentException(sprintf('Unable to convert data of type "%s" to DateTime object.', get_debug_type($value)));
+ }
+
+ protected function getOperatorExpression(string $queryPath, string $parameterName, Operator $operator, Expr $expr): object
+ {
+ $expression = match ($operator) {
+ Operator::Equals => $expr->eq(...),
+ Operator::NotEquals => $expr->neq(...),
+ Operator::GreaterThan => $expr->gt(...),
+ Operator::GreaterThanEquals => $expr->gte(...),
+ Operator::LessThan => $expr->lt(...),
+ Operator::LessThanEquals => $expr->lte(...),
+ default => throw new InvalidArgumentException('Operator not supported'),
+ };
+
+ return $expression($queryPath, ":$parameterName");
+ }
+
+ private function getFormattedActiveFilterString(FilterData $data, FilterInterface $filter, array $options): string
+ {
+ $value = $data->getValue();
+
+ if ($value instanceof \DateTimeInterface) {
+ $format = $options['form_options']['input_format'] ?? null;
+
+ if (null === $format) {
+ $format = 'Y-m-d H';
+
+ if ($options['form_options']['with_minutes'] ?? true) {
+ $format .= ':i';
+ }
+
+ if ($options['form_options']['with_seconds'] ?? true) {
+ $format .= ':s';
+ }
+ }
+
+ return $value->format($format);
+ }
+
+ return (string) $value;
}
}
diff --git a/src/Bridge/Doctrine/Orm/Filter/Type/DoctrineOrmFilterType.php b/src/Bridge/Doctrine/Orm/Filter/Type/DoctrineOrmFilterType.php
new file mode 100755
index 00000000..1e175087
--- /dev/null
+++ b/src/Bridge/Doctrine/Orm/Filter/Type/DoctrineOrmFilterType.php
@@ -0,0 +1,26 @@
+setDefault('auto_alias_resolving', true)
+ ->setAllowedTypes('auto_alias_resolving', 'bool')
+ ;
+ }
+}
diff --git a/src/Bridge/Doctrine/Orm/Filter/Type/EntityFilterType.php b/src/Bridge/Doctrine/Orm/Filter/Type/EntityFilterType.php
old mode 100644
new mode 100755
index 3294b5f6..81a667e5
--- a/src/Bridge/Doctrine/Orm/Filter/Type/EntityFilterType.php
+++ b/src/Bridge/Doctrine/Orm/Filter/Type/EntityFilterType.php
@@ -4,80 +4,89 @@
namespace Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type;
-use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQuery;
+use Doctrine\Bundle\DoctrineBundle\Registry;
+use Doctrine\ORM\Query\Expr;
+use Kreyu\Bundle\DataTableBundle\Exception\InvalidArgumentException;
use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
use Kreyu\Bundle\DataTableBundle\Filter\Operator;
-use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
-use Symfony\Bridge\Doctrine\Form\Type\EntityType as EntityFormType;
+use Symfony\Bridge\Doctrine\Form\Type\EntityType;
+use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PropertyAccess\PropertyAccess;
-class EntityFilterType extends AbstractFilterType
+class EntityFilterType extends AbstractDoctrineOrmFilterType
{
- /**
- * @param DoctrineOrmProxyQuery $query
- */
- public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter, array $options): void
- {
- $operator = $data->getOperator() ?? Operator::EQUALS;
- $value = $data->getValue();
-
- try {
- $expressionBuilderMethodName = $this->getExpressionBuilderMethodName($operator);
- } catch (\InvalidArgumentException) {
- return;
- }
-
- $parameterName = $this->getUniqueParameterName($query, $filter);
-
- $expression = $query->expr()->{$expressionBuilderMethodName}($this->getFilterQueryPath($query, $filter), ":$parameterName");
-
- $query
- ->andWhere($expression)
- ->setParameter($parameterName, $value);
+ public function __construct(
+ private readonly Registry $doctrineRegistry,
+ ) {
}
public function configureOptions(OptionsResolver $resolver): void
{
- $resolver->setDefaults([
- 'field_type' => EntityFormType::class,
- 'choice_label' => null,
- ]);
-
- $resolver->setDefault('operator_options', function (OptionsResolver $resolver) {
- $resolver->setDefaults([
- 'visible' => false,
- 'choices' => [
- Operator::EQUALS,
- Operator::NOT_EQUALS,
- Operator::CONTAINS,
- Operator::NOT_CONTAINS,
+ $resolver
+ ->setDefaults([
+ 'form_type' => EntityType::class,
+ 'supported_operators' => [
+ Operator::Equals,
+ Operator::NotEquals,
+ Operator::Contains,
+ Operator::NotContains,
],
- ]);
- });
+ 'choice_label' => null,
+ 'active_filter_formatter' => $this->getFormattedActiveFilterString(...),
+ ])
+ ->setAllowedTypes('choice_label', ['null', 'string', 'callable'])
+ ->addNormalizer('form_options', function (Options $options, array $value) {
+ if (EntityType::class !== $options['form_type']) {
+ return $value;
+ }
- $resolver->setDefault('active_filter_formatter', function (FilterData $data, FilterInterface $filter, array $options): mixed {
- $choiceLabel = $options['choice_label'];
+ // The identifier field name of the entity has to be provided in the 'choice_value' form option.
+ //
+ // This is required by the persistence system, because only the entity identifier will be persisted,
+ // and the EntityType form type needs to know how to convert it back to the entity object.
+ //
+ // If it's not provided, try to retrieve it from the entity metadata.
+ if (null === $value['choice_value'] ?? null) {
+ $identifiers = $this->doctrineRegistry
+ ->getManagerForClass($value['class'])
+ ?->getClassMetadata($value['class'])
+ ->getIdentifier() ?? [];
- if (is_string($choiceLabel)) {
- return PropertyAccess::createPropertyAccessor()->getValue($data->getValue(), $choiceLabel);
- }
+ if (1 === count($identifiers)) {
+ $value += ['choice_value' => reset($identifiers)];
+ }
+ }
- if (is_callable($choiceLabel)) {
- return $choiceLabel($data->getValue());
- }
-
- return $data->getValue();
- });
+ return $value;
+ })
+ ;
}
- private function getExpressionBuilderMethodName(Operator $operator): string
+ protected function getOperatorExpression(string $queryPath, string $parameterName, Operator $operator, Expr $expr): object
{
- return match ($operator) {
- Operator::EQUALS, Operator::CONTAINS => 'in',
- Operator::NOT_EQUALS, Operator::NOT_CONTAINS => 'notIn',
- default => throw new \InvalidArgumentException('Operator not supported'),
+ $expression = match ($operator) {
+ Operator::Equals, Operator::Contains => $expr->in(...),
+ Operator::NotEquals, Operator::NotContains => $expr->notIn(...),
+ default => throw new InvalidArgumentException('Operator not supported'),
};
+
+ return $expression($queryPath, ":$parameterName");
+ }
+
+ private function getFormattedActiveFilterString(FilterData $data, FilterInterface $filter, array $options): string
+ {
+ $choiceLabel = $options['choice_label'];
+
+ if (is_string($choiceLabel)) {
+ return PropertyAccess::createPropertyAccessor()->getValue($data->getValue(), $choiceLabel);
+ }
+
+ if (is_callable($choiceLabel)) {
+ return $choiceLabel($data->getValue());
+ }
+
+ return (string) $data->getValue();
}
}
diff --git a/src/Bridge/Doctrine/Orm/Filter/Type/NumericFilterType.php b/src/Bridge/Doctrine/Orm/Filter/Type/NumericFilterType.php
old mode 100644
new mode 100755
index 83563075..ece09469
--- a/src/Bridge/Doctrine/Orm/Filter/Type/NumericFilterType.php
+++ b/src/Bridge/Doctrine/Orm/Filter/Type/NumericFilterType.php
@@ -4,67 +4,43 @@
namespace Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type;
-use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQuery;
-use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
-use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
+use Doctrine\ORM\Query\Expr;
+use Kreyu\Bundle\DataTableBundle\Exception\InvalidArgumentException;
use Kreyu\Bundle\DataTableBundle\Filter\Operator;
-use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
-use Symfony\Component\Form\Extension\Core\Type\TextType;
+use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\OptionsResolver\OptionsResolver;
-class NumericFilterType extends AbstractFilterType
+class NumericFilterType extends AbstractDoctrineOrmFilterType
{
- /**
- * @param DoctrineOrmProxyQuery $query
- */
- public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter, array $options): void
- {
- $operator = $data->getOperator() ?? Operator::EQUALS;
- $value = $data->getValue();
-
- try {
- $expressionBuilderMethodName = $this->getExpressionBuilderMethodName($operator);
- } catch (\InvalidArgumentException) {
- return;
- }
-
- $parameterName = $this->getUniqueParameterName($query, $filter);
-
- $expression = $query->expr()->{$expressionBuilderMethodName}($this->getFilterQueryPath($query, $filter), ":$parameterName");
-
- $query
- ->andWhere($expression)
- ->setParameter($parameterName, $value);
- }
-
public function configureOptions(OptionsResolver $resolver): void
{
- $resolver->setDefault('field_type', TextType::class);
- $resolver->setDefault('operator_options', function (OptionsResolver $resolver) {
- $resolver->setDefaults([
- 'visible' => false,
- 'choices' => [
- Operator::EQUALS,
- Operator::NOT_EQUALS,
- Operator::GREATER_THAN_EQUALS,
- Operator::GREATER_THAN,
- Operator::LESS_THAN_EQUALS,
- Operator::LESS_THAN,
+ $resolver
+ ->setDefaults([
+ 'form_type' => NumberType::class,
+ 'supported_operators' => [
+ Operator::Equals,
+ Operator::NotEquals,
+ Operator::GreaterThanEquals,
+ Operator::GreaterThan,
+ Operator::LessThanEquals,
+ Operator::LessThan,
],
- ]);
- });
+ ])
+ ;
}
- private function getExpressionBuilderMethodName(Operator $operator): string
+ protected function getOperatorExpression(string $queryPath, string $parameterName, Operator $operator, Expr $expr): object
{
- return match ($operator) {
- Operator::EQUALS => 'eq',
- Operator::NOT_EQUALS => 'neq',
- Operator::GREATER_THAN_EQUALS => 'gte',
- Operator::GREATER_THAN => 'gt',
- Operator::LESS_THAN_EQUALS => 'lte',
- Operator::LESS_THAN => 'lt',
- default => throw new \InvalidArgumentException('Operator not supported'),
+ $expression = match ($operator) {
+ Operator::Equals => $expr->eq(...),
+ Operator::NotEquals => $expr->neq(...),
+ Operator::GreaterThanEquals => $expr->gte(...),
+ Operator::GreaterThan => $expr->gt(...),
+ Operator::LessThanEquals => $expr->lte(...),
+ Operator::LessThan => $expr->lt(...),
+ default => throw new InvalidArgumentException('Operator not supported'),
};
+
+ return $expression($queryPath, ":$parameterName");
}
}
diff --git a/src/Bridge/Doctrine/Orm/Filter/Type/StringFilterType.php b/src/Bridge/Doctrine/Orm/Filter/Type/StringFilterType.php
old mode 100644
new mode 100755
index 5f639a60..8b289076
--- a/src/Bridge/Doctrine/Orm/Filter/Type/StringFilterType.php
+++ b/src/Bridge/Doctrine/Orm/Filter/Type/StringFilterType.php
@@ -4,72 +4,49 @@
namespace Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type;
-use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQuery;
-use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
-use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
+use Doctrine\ORM\Query\Expr;
+use Kreyu\Bundle\DataTableBundle\Exception\InvalidArgumentException;
use Kreyu\Bundle\DataTableBundle\Filter\Operator;
-use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
-class StringFilterType extends AbstractFilterType
+class StringFilterType extends AbstractDoctrineOrmFilterType
{
- /**
- * @param DoctrineOrmProxyQuery $query
- */
- public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter, array $options): void
- {
- $operator = $data->getOperator() ?? Operator::EQUALS;
- $value = $data->getValue();
-
- try {
- $expressionBuilderMethodName = $this->getExpressionBuilderMethodName($operator);
- } catch (\InvalidArgumentException) {
- return;
- }
-
- $parameterName = $this->getUniqueParameterName($query, $filter);
-
- $expression = $query->expr()->{$expressionBuilderMethodName}($this->getFilterQueryPath($query, $filter), ":$parameterName");
-
- $query
- ->andWhere($expression)
- ->setParameter($parameterName, $this->getParameterValue($operator, $value));
- }
-
public function configureOptions(OptionsResolver $resolver): void
{
- $resolver->setDefault('operator_options', function (OptionsResolver $resolver) {
- $resolver->setDefaults([
- 'visible' => false,
- 'choices' => [
- Operator::EQUALS,
- Operator::NOT_EQUALS,
- Operator::CONTAINS,
- Operator::NOT_CONTAINS,
- Operator::STARTS_WITH,
- Operator::ENDS_WITH,
+ $resolver
+ ->setDefaults([
+ 'default_operator' => Operator::Contains,
+ 'supported_operators' => [
+ Operator::Equals,
+ Operator::NotEquals,
+ Operator::Contains,
+ Operator::NotContains,
+ Operator::StartsWith,
+ Operator::EndsWith,
],
- ]);
- });
+ ])
+ ;
}
- private function getExpressionBuilderMethodName(Operator $operator): string
+ protected function getOperatorExpression(string $queryPath, string $parameterName, Operator $operator, Expr $expr): object
{
- return match ($operator) {
- Operator::EQUALS => 'eq',
- Operator::NOT_EQUALS => 'neq',
- Operator::CONTAINS, Operator::STARTS_WITH, Operator::ENDS_WITH => 'like',
- Operator::NOT_CONTAINS => 'notLike',
- default => throw new \InvalidArgumentException('Operator not supported'),
+ $expression = match ($operator) {
+ Operator::Equals => $expr->eq(...),
+ Operator::NotEquals => $expr->neq(...),
+ Operator::Contains, Operator::StartsWith, Operator::EndsWith => $expr->like(...),
+ Operator::NotContains => $expr->notLike(...),
+ default => throw new InvalidArgumentException('Operator not supported'),
};
+
+ return $expression($queryPath, ":$parameterName");
}
- private function getParameterValue(Operator $operator, mixed $value): string
+ protected function getParameterValue(Operator $operator, mixed $value): string
{
return (string) match ($operator) {
- Operator::CONTAINS, Operator::NOT_CONTAINS => "%$value%",
- Operator::STARTS_WITH => "$value%",
- Operator::ENDS_WITH => "%$value",
+ Operator::Contains, Operator::NotContains => "%$value%",
+ Operator::StartsWith => "$value%",
+ Operator::EndsWith => "%$value",
default => $value,
};
}
diff --git a/src/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQuery.php b/src/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQuery.php
old mode 100644
new mode 100755
index 9ff9d383..6e524f46
--- a/src/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQuery.php
+++ b/src/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQuery.php
@@ -4,22 +4,24 @@
namespace Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query;
+use Doctrine\ORM\AbstractQuery;
+use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
-use Doctrine\ORM\Tools\Pagination\CountWalker;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Kreyu\Bundle\DataTableBundle\Pagination\CurrentPageOutOfRangeException;
use Kreyu\Bundle\DataTableBundle\Pagination\Pagination;
use Kreyu\Bundle\DataTableBundle\Pagination\PaginationData;
use Kreyu\Bundle\DataTableBundle\Pagination\PaginationInterface;
-use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
use Kreyu\Bundle\DataTableBundle\Sorting\SortingData;
/**
* @mixin QueryBuilder
*/
-class DoctrineOrmProxyQuery implements ProxyQueryInterface
+class DoctrineOrmProxyQuery implements DoctrineOrmProxyQueryInterface
{
private int $uniqueParameterId = 0;
+ private int $batchSize = 5000;
+ private bool $entityManagerClearingEnabled = true;
/**
* @param array $hints
@@ -27,6 +29,7 @@ class DoctrineOrmProxyQuery implements ProxyQueryInterface
public function __construct(
private QueryBuilder $queryBuilder,
private array $hints = [],
+ private string|int $hydrationMode = AbstractQuery::HYDRATE_OBJECT,
) {
}
@@ -61,13 +64,13 @@ public function sort(SortingData $sortingData): void
$this->queryBuilder->resetDQLPart('orderBy');
foreach ($sortingData->getColumns() as $column) {
- $field = $column->getName();
+ $propertyPath = (string) $column->getPropertyPath();
- if ($rootAlias && !str_contains($field, '.') && !str_starts_with($field, '__')) {
- $field = $rootAlias.'.'.$field;
+ if ($rootAlias && !str_contains($propertyPath, '.') && !str_starts_with($propertyPath, '__')) {
+ $propertyPath = $rootAlias.'.'.$propertyPath;
}
- $this->queryBuilder->addOrderBy($field, $column->getDirection());
+ $this->queryBuilder->addOrderBy($propertyPath, $column->getDirection());
}
}
@@ -100,6 +103,37 @@ public function getPagination(): PaginationInterface
return $this->getPagination();
}
+ public function getItems(): iterable
+ {
+ $paginator = $this->createPaginator();
+
+ $batchSize = $this->batchSize;
+
+ $cursorPosition = 0;
+
+ do {
+ $hasItems = true;
+
+ if (0 === $cursorPosition % $batchSize) {
+ $hasItems = false;
+
+ $paginator->getQuery()->setMaxResults($batchSize);
+ $paginator->getQuery()->setFirstResult($cursorPosition);
+
+ foreach ($paginator->getIterator() as $item) {
+ $hasItems = true;
+ yield $item;
+ }
+
+ if ($this->entityManagerClearingEnabled) {
+ $this->getEntityManager()->clear();
+ }
+ }
+
+ ++$cursorPosition;
+ } while (0 === $cursorPosition || $hasItems);
+ }
+
public function getUniqueParameterId(): int
{
return $this->uniqueParameterId++;
@@ -110,6 +144,31 @@ public function setHint(string $name, mixed $value): void
$this->hints[$name] = $value;
}
+ public function setHydrationMode(int|string $hydrationMode): void
+ {
+ $this->hydrationMode = $hydrationMode;
+ }
+
+ public function isEntityManagerClearingEnabled(): bool
+ {
+ return $this->entityManagerClearingEnabled;
+ }
+
+ public function setEntityManagerClearingEnabled(bool $entityManagerClearingEnabled): void
+ {
+ $this->entityManagerClearingEnabled = $entityManagerClearingEnabled;
+ }
+
+ public function getBatchSize(): int
+ {
+ return $this->batchSize;
+ }
+
+ public function setBatchSize(int $batchSize): void
+ {
+ $this->batchSize = $batchSize;
+ }
+
private function getCurrentPageNumber(): int
{
$firstResult = $this->queryBuilder->getFirstResult();
@@ -134,16 +193,19 @@ private function createPaginator(): Paginator
$hasSingleIdentifierName = 1 === \count($identifierFieldNames);
$hasJoins = \count($this->queryBuilder->getDQLPart('join')) > 0;
- $query = $this->queryBuilder->getQuery();
+ $query = (clone $this->queryBuilder)->getQuery();
- if (!$hasJoins) {
- $query->setHint(CountWalker::HINT_DISTINCT, false);
- }
+ $this->applyQueryHints($query);
+
+ $query->setHydrationMode($this->hydrationMode);
+ return new Paginator($query, $hasSingleIdentifierName && $hasJoins);
+ }
+
+ private function applyQueryHints(Query $query): void
+ {
foreach ($this->hints as $name => $value) {
$query->setHint($name, $value);
}
-
- return new Paginator($query, $hasSingleIdentifierName && $hasJoins);
}
}
diff --git a/src/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryFactory.php b/src/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryFactory.php
old mode 100644
new mode 100755
index 1dfb94aa..513399bf
--- a/src/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryFactory.php
+++ b/src/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryFactory.php
@@ -7,11 +7,10 @@
use Doctrine\ORM\QueryBuilder;
use Kreyu\Bundle\DataTableBundle\Exception\UnexpectedTypeException;
use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryFactoryInterface;
-use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
class DoctrineOrmProxyQueryFactory implements ProxyQueryFactoryInterface
{
- public function create(mixed $data): ProxyQueryInterface
+ public function create(mixed $data): DoctrineOrmProxyQueryInterface
{
if ($data instanceof QueryBuilder) {
return new DoctrineOrmProxyQuery($data);
diff --git a/src/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryInterface.php b/src/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryInterface.php
new file mode 100644
index 00000000..c797b37d
--- /dev/null
+++ b/src/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryInterface.php
@@ -0,0 +1,33 @@
+getTempnam($options));
-
- $writer = $this->getWriter($options);
- $writer->openToFile($path);
-
- if ($options['use_headers']) {
- /** @var HeaderRowView $headerRow */
- $headerRow = $view->vars['header_row'];
-
- $labels = [];
-
- foreach ($headerRow->children as $child) {
- if (false !== $child->vars['export']) {
- $label = $child->vars['export']['label'];
-
- if ($this->translator && $translationDomain = $child->vars['export']['translation_domain'] ?? null) {
- $label = $this->translator->trans($label, $child->vars['export']['translation_parameters'] ?? [], $translationDomain);
- }
-
- $labels[] = $label;
- }
- }
-
- $writer->addRow(Row::fromValues($labels));
- }
-
- foreach ($view->vars['value_rows'] as $valueRow) {
- $values = [];
-
- foreach ($valueRow->children as $child) {
- if (false !== $child->vars['export']) {
- $values[] = $child->vars['export']['value'];
- }
- }
-
- $writer->addRow(Row::fromValues($values));
- }
-
- $writer->close();
-
- $extension = $this->getExtension();
-
- return new ExportFile($path, "$filename.$extension");
- }
-
- abstract protected function getExtension(): string;
-
- abstract protected function getWriter(array $options): WriterInterface;
}
diff --git a/src/Bridge/OpenSpout/Exporter/Type/AbstractOpenSpoutExporterType.php b/src/Bridge/OpenSpout/Exporter/Type/AbstractOpenSpoutExporterType.php
new file mode 100644
index 00000000..a8e89372
--- /dev/null
+++ b/src/Bridge/OpenSpout/Exporter/Type/AbstractOpenSpoutExporterType.php
@@ -0,0 +1,112 @@
+getTempnam($options));
+
+ $writer = $this->getWriter($options);
+ $writer->openToFile($path);
+
+ if ($options['use_headers']) {
+ $writer->addRow(new Row(
+ cells: $this->getHeaderRowCells($view->headerRow, $options),
+ style: $this->getStyle($view->headerRow, 'header_row_style', $options)),
+ );
+ }
+
+ foreach ($view->valueRows as $valueRow) {
+ $writer->addRow(new Row(
+ cells: $this->getValueRowCells($valueRow, $options),
+ style: $this->getStyle($view->headerRow, 'value_row_style', $options)),
+ );
+ }
+
+ $writer->close();
+
+ return new ExportFile($path, sprintf('%s.%s', $filename, $this->getExtension()));
+ }
+
+ public function getParent(): ?string
+ {
+ return OpenSpoutExporterType::class;
+ }
+
+ protected function getWriter(array $options): WriterInterface
+ {
+ return new ($this->getWriterClass())($this->getWriterOptions($options));
+ }
+
+ abstract protected function getExtension(): string;
+
+ abstract protected function getWriterClass(): string;
+
+ abstract protected function getWriterOptions(array $options): mixed;
+
+ private function getHeaderRowCells(HeaderRowView $view, array $options): array
+ {
+ return array_map(
+ fn (ColumnHeaderView $columnHeaderView) => $this->getHeaderCell($columnHeaderView, $options),
+ $view->children,
+ );
+ }
+
+ protected function getHeaderCell(ColumnHeaderView $view, array $options): Cell
+ {
+ return Cell::fromValue(
+ value: $view->vars['label'],
+ style: $this->getStyle($view, 'header_cell_style', $options),
+ );
+ }
+
+ protected function getValueRowCells(ValueRowView $view, array $options): array
+ {
+ return array_map(
+ fn (ColumnValueView $columnValueView) => $this->getValueCell($columnValueView, $options),
+ $view->children,
+ );
+ }
+
+ protected function getValueCell(ColumnValueView $view, array $options): Cell
+ {
+ return Cell::fromValue(
+ value: $view->value,
+ style: $this->getStyle($view, 'value_cell_style', $options),
+ );
+ }
+
+ protected function getStyle(mixed $view, string $optionName, array $options): Style
+ {
+ $style = $options[$optionName] ?? null;
+
+ if (is_callable($style)) {
+ $style = $style($view, $options);
+ }
+
+ return $style ?? new Style();
+ }
+}
diff --git a/src/Bridge/OpenSpout/Exporter/Type/CsvExporterType.php b/src/Bridge/OpenSpout/Exporter/Type/CsvExporterType.php
index fc0eca8e..6330c1f0 100644
--- a/src/Bridge/OpenSpout/Exporter/Type/CsvExporterType.php
+++ b/src/Bridge/OpenSpout/Exporter/Type/CsvExporterType.php
@@ -4,44 +4,45 @@
namespace Kreyu\Bundle\DataTableBundle\Bridge\OpenSpout\Exporter\Type;
-use OpenSpout\Writer\CSV\Options;
-use OpenSpout\Writer\CSV\Writer;
-use OpenSpout\Writer\WriterInterface;
+use OpenSpout\Writer\CSV;
use Symfony\Component\OptionsResolver\OptionsResolver;
-class CsvExporterType extends AbstractExporterType
+class CsvExporterType extends AbstractOpenSpoutExporterType
{
- protected function getExtension(): string
+ public function configureOptions(OptionsResolver $resolver): void
{
- return 'csv';
+ $resolver
+ ->setDefaults([
+ 'field_delimiter' => ',',
+ 'field_enclosure' => '"',
+ 'should_add_bom' => true,
+ 'flush_threshold' => 500,
+ ])
+ ->setAllowedTypes('field_delimiter', 'string')
+ ->setAllowedTypes('field_enclosure', 'string')
+ ->setAllowedTypes('should_add_bom', 'bool')
+ ->setAllowedTypes('flush_threshold', 'int')
+ ;
+ }
+
+ protected function getWriterClass(): string
+ {
+ return CSV\Writer::class;
}
- protected function getWriter(array $options): WriterInterface
+ protected function getWriterOptions(array $options): CSV\Options
{
- $writerOptions = new Options();
+ $writerOptions = new CSV\Options();
$writerOptions->FIELD_DELIMITER = $options['field_delimiter'];
$writerOptions->FIELD_ENCLOSURE = $options['field_enclosure'];
$writerOptions->SHOULD_ADD_BOM = $options['should_add_bom'];
$writerOptions->FLUSH_THRESHOLD = $options['flush_threshold'];
- return new Writer($writerOptions);
+ return $writerOptions;
}
- public function configureOptions(OptionsResolver $resolver): void
+ protected function getExtension(): string
{
- $options = new Options();
-
- $resolver
- ->setDefaults([
- 'field_delimiter' => $options->FIELD_DELIMITER,
- 'field_enclosure' => $options->FIELD_ENCLOSURE,
- 'should_add_bom' => $options->SHOULD_ADD_BOM,
- 'flush_threshold' => $options->FLUSH_THRESHOLD,
- ])
- ->setAllowedTypes('field_delimiter', 'string')
- ->setAllowedTypes('field_enclosure', 'string')
- ->setAllowedTypes('should_add_bom', 'bool')
- ->setAllowedTypes('flush_threshold', 'int')
- ;
+ return 'csv';
}
}
diff --git a/src/Bridge/OpenSpout/Exporter/Type/OdsExporterType.php b/src/Bridge/OpenSpout/Exporter/Type/OdsExporterType.php
index c265e5c1..f36b1c6c 100644
--- a/src/Bridge/OpenSpout/Exporter/Type/OdsExporterType.php
+++ b/src/Bridge/OpenSpout/Exporter/Type/OdsExporterType.php
@@ -5,44 +5,45 @@
namespace Kreyu\Bundle\DataTableBundle\Bridge\OpenSpout\Exporter\Type;
use OpenSpout\Common\Entity\Style\Style;
-use OpenSpout\Writer\ODS\Options;
-use OpenSpout\Writer\ODS\Writer;
-use OpenSpout\Writer\WriterInterface;
+use OpenSpout\Writer\ODS;
use Symfony\Component\OptionsResolver\OptionsResolver;
-class OdsExporterType extends AbstractExporterType
+class OdsExporterType extends AbstractOpenSpoutExporterType
{
- protected function getExtension(): string
+ public function configureOptions(OptionsResolver $resolver): void
{
- return 'ods';
+ $resolver
+ ->setDefaults([
+ 'default_row_style' => new Style(),
+ 'should_create_new_sheets_automatically' => true,
+ 'default_column_width' => null,
+ 'default_row_height' => null,
+ ])
+ ->setAllowedTypes('default_row_style', Style::class)
+ ->setAllowedTypes('should_create_new_sheets_automatically', 'bool')
+ ->setAllowedTypes('default_column_width', ['null', 'float'])
+ ->setAllowedTypes('default_row_height', ['null', 'float'])
+ ;
+ }
+
+ protected function getWriterClass(): string
+ {
+ return ODS\Writer::class;
}
- protected function getWriter(array $options): WriterInterface
+ protected function getWriterOptions(array $options): ODS\Options
{
- $writerOptions = new Options();
+ $writerOptions = new ODS\Options();
$writerOptions->DEFAULT_ROW_STYLE = $options['default_row_style'];
$writerOptions->SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY = $options['should_create_new_sheets_automatically'];
$writerOptions->DEFAULT_COLUMN_WIDTH = $options['default_column_width'];
$writerOptions->DEFAULT_ROW_HEIGHT = $options['default_row_height'];
- return new Writer($writerOptions);
+ return $writerOptions;
}
- public function configureOptions(OptionsResolver $resolver): void
+ protected function getExtension(): string
{
- $options = new Options();
-
- $resolver
- ->setDefaults([
- 'default_row_style' => $options->DEFAULT_ROW_STYLE,
- 'should_create_new_sheets_automatically' => $options->SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY,
- 'default_column_width' => $options->DEFAULT_COLUMN_WIDTH,
- 'default_row_height' => $options->DEFAULT_ROW_HEIGHT,
- ])
- ->setAllowedTypes('default_row_style', Style::class)
- ->setAllowedTypes('should_create_new_sheets_automatically', 'bool')
- ->setAllowedTypes('default_column_width', ['null', 'float'])
- ->setAllowedTypes('default_row_height', ['null', 'float'])
- ;
+ return 'ods';
}
}
diff --git a/src/Bridge/OpenSpout/Exporter/Type/OpenSpoutExporterType.php b/src/Bridge/OpenSpout/Exporter/Type/OpenSpoutExporterType.php
new file mode 100644
index 00000000..b41aa737
--- /dev/null
+++ b/src/Bridge/OpenSpout/Exporter/Type/OpenSpoutExporterType.php
@@ -0,0 +1,42 @@
+setDefaults([
+ 'header_row_style' => null,
+ 'header_cell_style' => null,
+ 'value_row_style' => null,
+ 'value_cell_style' => null,
+ ])
+ ->setAllowedTypes('header_row_style', ['null', 'callable', Style::class])
+ ->setAllowedTypes('header_cell_style', ['null', 'callable', Style::class])
+ ->setAllowedTypes('value_row_style', ['null', 'callable', Style::class])
+ ->setAllowedTypes('value_cell_style', ['null', 'callable', Style::class])
+ ;
+ }
+}
diff --git a/src/Bridge/OpenSpout/Exporter/Type/XlsxExporterType.php b/src/Bridge/OpenSpout/Exporter/Type/XlsxExporterType.php
index 8d9408f3..e17973af 100644
--- a/src/Bridge/OpenSpout/Exporter/Type/XlsxExporterType.php
+++ b/src/Bridge/OpenSpout/Exporter/Type/XlsxExporterType.php
@@ -5,44 +5,48 @@
namespace Kreyu\Bundle\DataTableBundle\Bridge\OpenSpout\Exporter\Type;
use OpenSpout\Common\Entity\Style\Style;
-use OpenSpout\Writer\WriterInterface;
-use OpenSpout\Writer\XLSX\Options;
-use OpenSpout\Writer\XLSX\Writer;
+use OpenSpout\Writer\XLSX;
use Symfony\Component\OptionsResolver\OptionsResolver;
-class XlsxExporterType extends AbstractExporterType
+class XlsxExporterType extends AbstractOpenSpoutExporterType
{
- protected function getExtension(): string
+ public function configureOptions(OptionsResolver $resolver): void
{
- return 'xlsx';
+ $resolver
+ ->setDefaults([
+ 'default_row_style' => new Style(),
+ 'should_create_new_sheets_automatically' => true,
+ 'should_use_inline_strings' => true,
+ 'default_column_width' => null,
+ 'default_row_height' => null,
+ ])
+ ->setAllowedTypes('default_row_style', Style::class)
+ ->setAllowedTypes('should_create_new_sheets_automatically', 'bool')
+ ->setAllowedTypes('should_use_inline_strings', 'bool')
+ ->setAllowedTypes('default_column_width', ['null', 'float'])
+ ->setAllowedTypes('default_row_height', ['null', 'float'])
+ ;
+ }
+
+ protected function getWriterClass(): string
+ {
+ return XLSX\Writer::class;
}
- protected function getWriter(array $options): WriterInterface
+ protected function getWriterOptions(array $options): XLSX\Options
{
- $writerOptions = new Options();
+ $writerOptions = new XLSX\Options();
$writerOptions->DEFAULT_ROW_STYLE = $options['default_row_style'];
$writerOptions->SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY = $options['should_create_new_sheets_automatically'];
+ $writerOptions->SHOULD_USE_INLINE_STRINGS = $options['should_use_inline_strings'];
$writerOptions->DEFAULT_COLUMN_WIDTH = $options['default_column_width'];
$writerOptions->DEFAULT_ROW_HEIGHT = $options['default_row_height'];
- return new Writer($writerOptions);
+ return $writerOptions;
}
- public function configureOptions(OptionsResolver $resolver): void
+ protected function getExtension(): string
{
- $options = new Options();
-
- $resolver
- ->setDefaults([
- 'default_row_style' => $options->DEFAULT_ROW_STYLE,
- 'should_create_new_sheets_automatically' => $options->SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY,
- 'default_column_width' => $options->DEFAULT_COLUMN_WIDTH,
- 'default_row_height' => $options->DEFAULT_ROW_HEIGHT,
- ])
- ->setAllowedTypes('default_row_style', Style::class)
- ->setAllowedTypes('should_create_new_sheets_automatically', 'bool')
- ->setAllowedTypes('default_column_width', ['null', 'float'])
- ->setAllowedTypes('default_row_height', ['null', 'float'])
- ;
+ return 'xlsx';
}
}
diff --git a/src/Bridge/PhpSpreadsheet/Exporter/Type/AbstractExporterType.php b/src/Bridge/PhpSpreadsheet/Exporter/Type/AbstractExporterType.php
index 63d1d6eb..a179eea2 100644
--- a/src/Bridge/PhpSpreadsheet/Exporter/Type/AbstractExporterType.php
+++ b/src/Bridge/PhpSpreadsheet/Exporter/Type/AbstractExporterType.php
@@ -4,111 +4,9 @@
namespace Kreyu\Bundle\DataTableBundle\Bridge\PhpSpreadsheet\Exporter\Type;
-use Kreyu\Bundle\DataTableBundle\Column\ColumnHeaderView;
-use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView;
-use Kreyu\Bundle\DataTableBundle\DataTableView;
-use Kreyu\Bundle\DataTableBundle\Exporter\ExportFile;
-use Kreyu\Bundle\DataTableBundle\Exporter\Type\AbstractExporterType as BaseAbstractType;
-use Kreyu\Bundle\DataTableBundle\HeaderRowView;
-use PhpOffice\PhpSpreadsheet\Exception;
-use PhpOffice\PhpSpreadsheet\Spreadsheet;
-use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
-use PhpOffice\PhpSpreadsheet\Writer\IWriter;
-use Symfony\Contracts\Translation\TranslatorInterface;
-
-abstract class AbstractExporterType extends BaseAbstractType
+/**
+ * @deprecated since 0.14.0, use {@see AbstractPhpSpreadsheetExporterType}
+ */
+abstract class AbstractExporterType extends AbstractPhpSpreadsheetExporterType
{
- public function __construct(
- private ?TranslatorInterface $translator = null,
- ) {
- }
-
- abstract protected function getWriter(Spreadsheet $spreadsheet, array $options): IWriter;
-
- /**
- * @throws Exception
- */
- public function export(DataTableView $view, string $filename, array $options = []): ExportFile
- {
- $spreadsheet = $this->createSpreadsheet($view, $options);
-
- $writer = $this->getWriter($spreadsheet, $options);
- $writer->setPreCalculateFormulas($options['pre_calculate_formulas']);
-
- $writer->save($tempnam = $this->getTempnam($options));
-
- $extension = mb_strtolower((new \ReflectionClass($writer))->getShortName());
-
- return new ExportFile($tempnam, "$filename.$extension");
- }
-
- /**
- * @throws Exception
- */
- protected function createSpreadsheet(DataTableView $view, array $options = []): Spreadsheet
- {
- $spreadsheet = new Spreadsheet();
-
- $worksheet = $spreadsheet->getActiveSheet();
-
- if ($options['use_headers']) {
- /** @var HeaderRowView $headerRow */
- $headerRow = $view->vars['header_row'];
-
- $headers = array_filter($headerRow->children, function (ColumnHeaderView $view) {
- return false !== $view->vars['export'];
- });
-
- $this->appendRow(
- $worksheet,
- array_map(function (ColumnHeaderView $view) {
- $label = $view->vars['export']['label'];
-
- if ($this->translator && $translationDomain = $view->vars['export']['translation_domain'] ?? null) {
- $label = $this->translator->trans($label, $view->vars['export']['translation_parameters'] ?? [], $translationDomain);
- }
-
- return $label;
- }, $headers),
- );
- }
-
- foreach ($view->vars['value_rows'] as $valueRow) {
- $values = array_filter($valueRow->children, function (ColumnValueView $view) {
- return false !== $view->vars['export'];
- });
-
- $this->appendRow(
- $worksheet,
- array_map(fn (ColumnValueView $view) => $view->vars['export']['value'], $values),
- );
- }
-
- return $spreadsheet;
- }
-
- /**
- * @throws Exception
- */
- protected function appendRow(Worksheet $worksheet, array $data): void
- {
- $row = $worksheet->getHighestRow();
-
- $worksheet->insertNewRowBefore($row);
-
- $index = 0;
-
- foreach ($data as $value) {
- if (is_array($value)) {
- $value = implode(', ', $value);
- }
-
- $worksheet->setCellValue([++$index, $row], $value);
- }
- }
-
- public function getParent(): ?string
- {
- return PhpSpreadsheetExporterType::class;
- }
}
diff --git a/src/Bridge/PhpSpreadsheet/Exporter/Type/AbstractPhpSpreadsheetExporterType.php b/src/Bridge/PhpSpreadsheet/Exporter/Type/AbstractPhpSpreadsheetExporterType.php
new file mode 100755
index 00000000..fabe9431
--- /dev/null
+++ b/src/Bridge/PhpSpreadsheet/Exporter/Type/AbstractPhpSpreadsheetExporterType.php
@@ -0,0 +1,89 @@
+createSpreadsheet($view, $options);
+
+ $writer = $this->getWriter($spreadsheet, $options);
+ $writer->setPreCalculateFormulas($options['pre_calculate_formulas']);
+
+ $writer->save($tempnam = $this->getTempnam($options));
+
+ $extension = mb_strtolower((new \ReflectionClass($writer))->getShortName());
+
+ return new ExportFile($tempnam, "$filename.$extension");
+ }
+
+ /**
+ * @throws Exception
+ */
+ protected function createSpreadsheet(DataTableView $view, array $options = []): Spreadsheet
+ {
+ $spreadsheet = new Spreadsheet();
+
+ $worksheet = $spreadsheet->getActiveSheet();
+
+ if ($options['use_headers']) {
+ $this->appendRow($worksheet, array_map(
+ static fn (ColumnHeaderView $view) => $view->vars['label'],
+ $view->headerRow->children,
+ ));
+ }
+
+ foreach ($view->valueRows as $valueRow) {
+ $this->appendRow($worksheet, array_map(
+ static fn (ColumnValueView $view) => $view->value,
+ $valueRow->children,
+ ));
+ }
+
+ return $spreadsheet;
+ }
+
+ /**
+ * @throws Exception
+ */
+ protected function appendRow(Worksheet $worksheet, array $data): void
+ {
+ $row = $worksheet->getHighestRow();
+
+ $worksheet->insertNewRowBefore($row);
+
+ $index = 0;
+
+ foreach ($data as $value) {
+ if (is_array($value)) {
+ $value = implode(', ', $value);
+ }
+
+ $worksheet->setCellValue([++$index, $row], $value);
+ }
+ }
+
+ public function getParent(): ?string
+ {
+ return PhpSpreadsheetExporterType::class;
+ }
+}
diff --git a/src/Bridge/PhpSpreadsheet/Exporter/Type/CsvExporterType.php b/src/Bridge/PhpSpreadsheet/Exporter/Type/CsvExporterType.php
old mode 100644
new mode 100755
index 1b7a91d2..74ce5641
--- a/src/Bridge/PhpSpreadsheet/Exporter/Type/CsvExporterType.php
+++ b/src/Bridge/PhpSpreadsheet/Exporter/Type/CsvExporterType.php
@@ -10,7 +10,7 @@
use PhpOffice\PhpSpreadsheet\Writer\IWriter;
use Symfony\Component\OptionsResolver\OptionsResolver;
-class CsvExporterType extends AbstractExporterType
+class CsvExporterType extends AbstractPhpSpreadsheetExporterType
{
public function configureOptions(OptionsResolver $resolver): void
{
diff --git a/src/Bridge/PhpSpreadsheet/Exporter/Type/HtmlExporterType.php b/src/Bridge/PhpSpreadsheet/Exporter/Type/HtmlExporterType.php
old mode 100644
new mode 100755
index 1bcdbbab..266c9a00
--- a/src/Bridge/PhpSpreadsheet/Exporter/Type/HtmlExporterType.php
+++ b/src/Bridge/PhpSpreadsheet/Exporter/Type/HtmlExporterType.php
@@ -10,7 +10,7 @@
use PhpOffice\PhpSpreadsheet\Writer\IWriter;
use Symfony\Component\OptionsResolver\OptionsResolver;
-class HtmlExporterType extends AbstractExporterType
+class HtmlExporterType extends AbstractPhpSpreadsheetExporterType
{
public function configureOptions(OptionsResolver $resolver): void
{
diff --git a/src/Bridge/PhpSpreadsheet/Exporter/Type/OdsExporterType.php b/src/Bridge/PhpSpreadsheet/Exporter/Type/OdsExporterType.php
old mode 100644
new mode 100755
index 53bfff29..5b7427b4
--- a/src/Bridge/PhpSpreadsheet/Exporter/Type/OdsExporterType.php
+++ b/src/Bridge/PhpSpreadsheet/Exporter/Type/OdsExporterType.php
@@ -8,7 +8,7 @@
use PhpOffice\PhpSpreadsheet\Writer\IWriter;
use PhpOffice\PhpSpreadsheet\Writer\Ods;
-class OdsExporterType extends AbstractExporterType
+class OdsExporterType extends AbstractPhpSpreadsheetExporterType
{
protected function getWriter(Spreadsheet $spreadsheet, array $options): IWriter
{
diff --git a/src/Bridge/PhpSpreadsheet/Exporter/Type/PdfExporterType.php b/src/Bridge/PhpSpreadsheet/Exporter/Type/PdfExporterType.php
old mode 100644
new mode 100755
index f4802f14..9e8daeb3
--- a/src/Bridge/PhpSpreadsheet/Exporter/Type/PdfExporterType.php
+++ b/src/Bridge/PhpSpreadsheet/Exporter/Type/PdfExporterType.php
@@ -12,7 +12,7 @@
use PhpOffice\PhpSpreadsheet\Writer\Pdf\Tcpdf;
use Symfony\Component\OptionsResolver\OptionsResolver;
-class PdfExporterType extends AbstractExporterType
+class PdfExporterType extends AbstractPhpSpreadsheetExporterType
{
public function configureOptions(OptionsResolver $resolver): void
{
diff --git a/src/Bridge/PhpSpreadsheet/Exporter/Type/PhpSpreadsheetExporterType.php b/src/Bridge/PhpSpreadsheet/Exporter/Type/PhpSpreadsheetExporterType.php
old mode 100644
new mode 100755
index ae3a337d..4b000dbd
--- a/src/Bridge/PhpSpreadsheet/Exporter/Type/PhpSpreadsheetExporterType.php
+++ b/src/Bridge/PhpSpreadsheet/Exporter/Type/PhpSpreadsheetExporterType.php
@@ -5,15 +5,15 @@
namespace Kreyu\Bundle\DataTableBundle\Bridge\PhpSpreadsheet\Exporter\Type;
use Kreyu\Bundle\DataTableBundle\DataTableView;
+use Kreyu\Bundle\DataTableBundle\Exporter\ExporterInterface;
use Kreyu\Bundle\DataTableBundle\Exporter\ExportFile;
-use Kreyu\Bundle\DataTableBundle\Exporter\Type\ExporterType as BaseExporterType;
-use Kreyu\Bundle\DataTableBundle\Exporter\Type\ExporterTypeInterface;
+use Kreyu\Bundle\DataTableBundle\Exporter\Type\AbstractExporterType;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use Symfony\Component\OptionsResolver\OptionsResolver;
-final class PhpSpreadsheetExporterType implements ExporterTypeInterface
+final class PhpSpreadsheetExporterType extends AbstractExporterType
{
- public function export(DataTableView $view, string $filename, array $options = []): ExportFile
+ public function export(DataTableView $view, ExporterInterface $exporter, string $filename, array $options = []): ExportFile
{
throw new \LogicException('Base exporter type cannot be called directly');
}
@@ -31,9 +31,4 @@ public function configureOptions(OptionsResolver $resolver): void
->setAllowedTypes('pre_calculate_formulas', 'bool')
;
}
-
- public function getParent(): ?string
- {
- return BaseExporterType::class;
- }
}
diff --git a/src/Bridge/PhpSpreadsheet/Exporter/Type/XlsExporterType.php b/src/Bridge/PhpSpreadsheet/Exporter/Type/XlsExporterType.php
old mode 100644
new mode 100755
index a72cc246..f136e7a4
--- a/src/Bridge/PhpSpreadsheet/Exporter/Type/XlsExporterType.php
+++ b/src/Bridge/PhpSpreadsheet/Exporter/Type/XlsExporterType.php
@@ -8,7 +8,7 @@
use PhpOffice\PhpSpreadsheet\Writer\IWriter;
use PhpOffice\PhpSpreadsheet\Writer\Xls;
-class XlsExporterType extends AbstractExporterType
+class XlsExporterType extends AbstractPhpSpreadsheetExporterType
{
protected function getWriter(Spreadsheet $spreadsheet, array $options): IWriter
{
diff --git a/src/Bridge/PhpSpreadsheet/Exporter/Type/XlsxExporterType.php b/src/Bridge/PhpSpreadsheet/Exporter/Type/XlsxExporterType.php
old mode 100644
new mode 100755
index 599b457f..aaea1e6d
--- a/src/Bridge/PhpSpreadsheet/Exporter/Type/XlsxExporterType.php
+++ b/src/Bridge/PhpSpreadsheet/Exporter/Type/XlsxExporterType.php
@@ -9,7 +9,7 @@
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Symfony\Component\OptionsResolver\OptionsResolver;
-class XlsxExporterType extends AbstractExporterType
+class XlsxExporterType extends AbstractPhpSpreadsheetExporterType
{
public function configureOptions(OptionsResolver $resolver): void
{
diff --git a/src/Column/Column.php b/src/Column/Column.php
old mode 100644
new mode 100755
index 3693dafa..fda4f070
--- a/src/Column/Column.php
+++ b/src/Column/Column.php
@@ -4,49 +4,131 @@
namespace Kreyu\Bundle\DataTableBundle\Column;
-use Kreyu\Bundle\DataTableBundle\Column\Type\ResolvedColumnTypeInterface;
+use Kreyu\Bundle\DataTableBundle\DataTableInterface;
+use Kreyu\Bundle\DataTableBundle\Exception\BadMethodCallException;
use Kreyu\Bundle\DataTableBundle\HeaderRowView;
use Kreyu\Bundle\DataTableBundle\ValueRowView;
+use Symfony\Component\PropertyAccess\PropertyPath;
+use Symfony\Component\PropertyAccess\PropertyPathInterface;
class Column implements ColumnInterface
{
+ private ?DataTableInterface $dataTable = null;
+ private ?PropertyPathInterface $propertyPath = null;
+ private ?PropertyPathInterface $sortPropertyPath = null;
+ private int $priority = 0;
+ private bool $visible = true;
+
public function __construct(
- private string $name,
- private ResolvedColumnTypeInterface $type,
- private array $options = [],
+ private readonly ColumnConfigInterface $config,
) {
}
public function getName(): string
{
- return $this->name;
+ return $this->config->getName();
+ }
+
+ public function getConfig(): ColumnConfigInterface
+ {
+ return $this->config;
}
- public function getType(): ResolvedColumnTypeInterface
+ public function getDataTable(): DataTableInterface
{
- return $this->type;
+ if (null === $this->dataTable) {
+ throw new BadMethodCallException('Column is not attached to any data table.');
+ }
+
+ return $this->dataTable;
}
- public function getOptions(): array
+ public function setDataTable(DataTableInterface $dataTable): static
{
- return $this->options;
+ $this->dataTable = $dataTable;
+
+ return $this;
+ }
+
+ public function getPropertyPath(): ?PropertyPathInterface
+ {
+ if ($this->propertyPath || $this->propertyPath = $this->config->getPropertyPath()) {
+ return $this->propertyPath;
+ }
+
+ if ('' === $name = $this->getName()) {
+ return null;
+ }
+
+ return $this->propertyPath = new PropertyPath($name);
+ }
+
+ public function getSortPropertyPath(): ?PropertyPathInterface
+ {
+ if ($this->sortPropertyPath || $this->sortPropertyPath = $this->config->getSortPropertyPath()) {
+ return $this->sortPropertyPath;
+ }
+
+ return $this->sortPropertyPath = $this->getPropertyPath();
}
public function createHeaderView(HeaderRowView $parent = null): ColumnHeaderView
{
- $view = $this->type->createHeaderView($this, $parent);
+ $view = $this->config->getType()->createHeaderView($this, $parent);
- $this->type->buildHeaderView($view, $this, $this->options);
+ $this->config->getType()->buildHeaderView($view, $this, $this->config->getOptions());
return $view;
}
public function createValueView(ValueRowView $parent = null): ColumnValueView
{
- $view = $this->type->createValueView($this, $parent);
+ $view = $this->config->getType()->createValueView($this, $parent);
+
+ $this->config->getType()->buildValueView($view, $this, $this->config->getOptions());
+
+ return $view;
+ }
+
+ public function createExportHeaderView(HeaderRowView $parent = null): ColumnHeaderView
+ {
+ $view = $this->config->getType()->createExportHeaderView($this, $parent);
- $this->type->buildValueView($view, $this, $this->options);
+ $this->config->getType()->buildExportHeaderView($view, $this, $this->config->getOptions());
return $view;
}
+
+ public function createExportValueView(ValueRowView $parent = null): ColumnValueView
+ {
+ $view = $this->config->getType()->createExportValueView($this, $parent);
+
+ $this->config->getType()->buildExportValueView($view, $this, $this->config->getOptions());
+
+ return $view;
+ }
+
+ public function getPriority(): int
+ {
+ return $this->priority;
+ }
+
+ public function setPriority(int $priority): static
+ {
+ $this->priority = $priority;
+
+ return $this;
+ }
+
+ public function isVisible(): bool
+ {
+ return $this->visible;
+ }
+
+ public function setVisible(bool $visible): static
+ {
+ $this->visible = $visible;
+
+ return $this;
+ }
}
diff --git a/src/Column/ColumnBuilder.php b/src/Column/ColumnBuilder.php
new file mode 100755
index 00000000..54d1eb76
--- /dev/null
+++ b/src/Column/ColumnBuilder.php
@@ -0,0 +1,70 @@
+locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ return $this->priority;
+ }
+
+ public function setPriority(int $priority): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->priority = $priority;
+
+ return $this;
+ }
+
+ public function isVisible(): bool
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ return $this->visible;
+ }
+
+ public function setVisible(bool $visible): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->visible = $visible;
+
+ return $this;
+ }
+
+ public function getColumn(): ColumnInterface
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ return (new Column($this->getColumnConfig()))
+ ->setPriority($this->getPriority())
+ ->setVisible($this->isVisible())
+ ;
+ }
+
+ private function createBuilderLockedException(): BadMethodCallException
+ {
+ return new BadMethodCallException('ColumnBuilder methods cannot be accessed anymore once the builder is turned into a ColumnConfigInterface instance.');
+ }
+}
diff --git a/src/Column/ColumnBuilderInterface.php b/src/Column/ColumnBuilderInterface.php
new file mode 100755
index 00000000..cf067a78
--- /dev/null
+++ b/src/Column/ColumnBuilderInterface.php
@@ -0,0 +1,18 @@
+name;
+ }
+
+ public function setName(string $name): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->name = $name;
+
+ return $this;
+ }
+
+ public function getType(): ResolvedColumnTypeInterface
+ {
+ return $this->type;
+ }
+
+ public function setType(ResolvedColumnTypeInterface $type): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->type = $type;
+
+ return $this;
+ }
+
+ public function getOptions(): array
+ {
+ return $this->options;
+ }
+
+ public function hasOption(string $name): bool
+ {
+ return array_key_exists($name, $this->options);
+ }
+
+ public function getOption(string $name, mixed $default = null): mixed
+ {
+ return $this->options[$name] ?? $default;
+ }
+
+ public function setOptions(array $options): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->options = $options;
+
+ return $this;
+ }
+
+ public function setOption(string $name, mixed $value): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->options[$name] = $value;
+
+ return $this;
+ }
+
+ public function getAttributes(): array
+ {
+ return $this->attributes;
+ }
+
+ public function hasAttribute(string $name): bool
+ {
+ return array_key_exists($name, $this->attributes);
+ }
+
+ public function getAttribute(string $name, mixed $default = null): mixed
+ {
+ return $this->attributes[$name] ?? $default;
+ }
+
+ public function setAttributes(array $attributes): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->attributes = $attributes;
+
+ return $this;
+ }
+
+ public function setAttribute(string $name, mixed $value): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->attributes[$name] = $value;
+
+ return $this;
+ }
+
+ public function getPropertyPath(): ?PropertyPathInterface
+ {
+ return $this->propertyPath;
+ }
+
+ public function setPropertyPath(null|string|PropertyPathInterface $propertyPath): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ if (is_string($propertyPath)) {
+ $propertyPath = new PropertyPath($propertyPath);
+ }
+
+ $this->propertyPath = $propertyPath;
+
+ return $this;
+ }
+
+ public function getSortPropertyPath(): ?PropertyPathInterface
+ {
+ return $this->sortPropertyPath;
+ }
+
+ public function setSortPropertyPath(null|string|PropertyPathInterface $sortPropertyPath): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ if (is_string($sortPropertyPath)) {
+ $sortPropertyPath = new PropertyPath($sortPropertyPath);
+ }
+
+ $this->sortPropertyPath = $sortPropertyPath;
+
+ return $this;
+ }
+
+ public function isSortable(): bool
+ {
+ return $this->sortable;
+ }
+
+ public function setSortable(bool $sortable): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->sortable = $sortable;
+
+ return $this;
+ }
+
+ public function isExportable(): bool
+ {
+ return $this->exportable;
+ }
+
+ public function setExportable(bool $exportable): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->exportable = $exportable;
+
+ return $this;
+ }
+
+ public function isPersonalizable(): bool
+ {
+ return $this->personalizable;
+ }
+
+ public function setPersonalizable(bool $personalizable): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->personalizable = $personalizable;
+
+ return $this;
+ }
+
+ public function getColumnFactory(): ColumnFactoryInterface
+ {
+ if (!isset($this->columnFactory)) {
+ throw new BadMethodCallException('The column factory must be set before retrieving it.');
+ }
+
+ return $this->columnFactory;
+ }
+
+ public function setColumnFactory(ColumnFactoryInterface $columnFactory): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->columnFactory = $columnFactory;
+
+ return $this;
+ }
+
+ public function getColumnConfig(): ColumnConfigInterface
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $config = clone $this;
+ $config->locked = true;
+
+ return $config;
+ }
+
+ private function createBuilderLockedException(): BadMethodCallException
+ {
+ return new BadMethodCallException('ColumnConfigBuilder methods cannot be accessed anymore once the builder is turned into a ColumnConfigInterface instance.');
+ }
+}
diff --git a/src/Column/ColumnConfigBuilderInterface.php b/src/Column/ColumnConfigBuilderInterface.php
new file mode 100755
index 00000000..75d041b6
--- /dev/null
+++ b/src/Column/ColumnConfigBuilderInterface.php
@@ -0,0 +1,46 @@
+ $type
- */
- public function create(string $name, string $type, array $options = []): ColumnInterface
+ public function create(string $type = ColumnType::class, array $options = []): ColumnInterface
+ {
+ return $this->createBuilder($type, $options)->getColumn();
+ }
+
+ public function createNamed(string $name, string $type = ColumnType::class, array $options = []): ColumnInterface
+ {
+ return $this->createNamedBuilder($name, $type, $options)->getColumn();
+ }
+
+ public function createBuilder(string $type = ColumnType::class, array $options = []): ColumnBuilderInterface
+ {
+ return $this->createNamedBuilder($this->registry->getType($type)->getBlockPrefix(), $type, $options);
+ }
+
+ public function createNamedBuilder(string $name, string $type = ColumnType::class, array $options = []): ColumnBuilderInterface
{
$type = $this->registry->getType($type);
- $optionsResolver = $type->getOptionsResolver();
+ $builder = $type->createBuilder($this, $name, $options);
+
+ $type->buildColumn($builder, $builder->getOptions());
- return new Column($name, $type, $optionsResolver->resolve($options));
+ return $builder;
}
}
diff --git a/src/Column/ColumnFactoryAwareInterface.php b/src/Column/ColumnFactoryAwareInterface.php
old mode 100644
new mode 100755
diff --git a/src/Column/ColumnFactoryAwareTrait.php b/src/Column/ColumnFactoryAwareTrait.php
old mode 100644
new mode 100755
diff --git a/src/Column/ColumnFactoryBuilder.php b/src/Column/ColumnFactoryBuilder.php
new file mode 100644
index 00000000..1cd4b72c
--- /dev/null
+++ b/src/Column/ColumnFactoryBuilder.php
@@ -0,0 +1,121 @@
+
+ */
+ private array $extensions = [];
+
+ /**
+ * @var array
+ */
+ private array $types = [];
+
+ /**
+ * @var array>
+ */
+ private array $typeExtensions = [];
+
+ public function __construct(
+ private readonly bool $forceCoreExtension = false,
+ ) {
+ }
+
+ public function setResolvedTypeFactory(ResolvedColumnTypeFactoryInterface $resolvedTypeFactory): static
+ {
+ $this->resolvedTypeFactory = $resolvedTypeFactory;
+
+ return $this;
+ }
+
+ public function addExtension(ColumnExtensionInterface $extension): static
+ {
+ $this->extensions[] = $extension;
+
+ return $this;
+ }
+
+ public function addExtensions(array $extensions): static
+ {
+ $this->extensions = array_merge($this->extensions, $extensions);
+
+ return $this;
+ }
+
+ public function addType(ColumnTypeInterface $type): static
+ {
+ $this->types[] = $type;
+
+ return $this;
+ }
+
+ public function addTypes(array $types): static
+ {
+ foreach ($types as $type) {
+ $this->types[] = $type;
+ }
+
+ return $this;
+ }
+
+ public function addTypeExtension(ColumnTypeExtensionInterface $typeExtension): static
+ {
+ foreach ($typeExtension::getExtendedTypes() as $extendedType) {
+ $this->typeExtensions[$extendedType][] = $typeExtension;
+ }
+
+ return $this;
+ }
+
+ public function addTypeExtensions(array $typeExtensions): static
+ {
+ foreach ($typeExtensions as $typeExtension) {
+ $this->addTypeExtension($typeExtension);
+ }
+
+ return $this;
+ }
+
+ public function getColumnFactory(): ColumnFactoryInterface
+ {
+ $extensions = $this->extensions;
+
+ if ($this->forceCoreExtension) {
+ $hasCoreExtension = false;
+
+ foreach ($extensions as $extension) {
+ if ($extension instanceof CoreColumnExtension) {
+ $hasCoreExtension = true;
+ break;
+ }
+ }
+
+ if (!$hasCoreExtension) {
+ array_unshift($extensions, new CoreColumnExtension());
+ }
+ }
+
+ if (\count($this->types) > 0 || \count($this->typeExtensions) > 0) {
+ $extensions[] = new PreloadedColumnExtension($this->types, $this->typeExtensions);
+ }
+
+ $registry = new ColumnRegistry($extensions, $this->resolvedTypeFactory ?? new ResolvedColumnTypeFactory());
+
+ return new ColumnFactory($registry);
+ }
+}
diff --git a/src/Column/ColumnFactoryBuilderInterface.php b/src/Column/ColumnFactoryBuilderInterface.php
new file mode 100644
index 00000000..b7a64b07
--- /dev/null
+++ b/src/Column/ColumnFactoryBuilderInterface.php
@@ -0,0 +1,38 @@
+ $extensions
+ */
+ public function addExtensions(array $extensions): static;
+
+ public function addType(ColumnTypeInterface $type): static;
+
+ /**
+ * @param array $types
+ */
+ public function addTypes(array $types): static;
+
+ public function addTypeExtension(ColumnTypeExtensionInterface $typeExtension): static;
+
+ /**
+ * @param array $typeExtensions
+ */
+ public function addTypeExtensions(array $typeExtensions): static;
+
+ public function getColumnFactory(): ColumnFactoryInterface;
+}
diff --git a/src/Column/ColumnFactoryInterface.php b/src/Column/ColumnFactoryInterface.php
old mode 100644
new mode 100755
index e23b8e24..6c7c3b1d
--- a/src/Column/ColumnFactoryInterface.php
+++ b/src/Column/ColumnFactoryInterface.php
@@ -4,12 +4,37 @@
namespace Kreyu\Bundle\DataTableBundle\Column;
+use Kreyu\Bundle\DataTableBundle\Column\Type\ColumnType;
use Kreyu\Bundle\DataTableBundle\Column\Type\ColumnTypeInterface;
+use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
interface ColumnFactoryInterface
{
/**
* @param class-string $type
+ *
+ * @throws InvalidOptionsException if any of given option is not applicable to the given type
*/
- public function create(string $name, string $type, array $options = []): ColumnInterface;
+ public function create(string $type = ColumnType::class, array $options = []): ColumnInterface;
+
+ /**
+ * @param class-string $type
+ *
+ * @throws InvalidOptionsException if any of given option is not applicable to the given type
+ */
+ public function createNamed(string $name, string $type = ColumnType::class, array $options = []): ColumnInterface;
+
+ /**
+ * @param class-string $type
+ *
+ * @throws InvalidOptionsException if any of given option is not applicable to the given type
+ */
+ public function createBuilder(string $type = ColumnType::class, array $options = []): ColumnBuilderInterface;
+
+ /**
+ * @param class-string $type
+ *
+ * @throws InvalidOptionsException if any of given option is not applicable to the given type
+ */
+ public function createNamedBuilder(string $name, string $type = ColumnType::class, array $options = []): ColumnBuilderInterface;
}
diff --git a/src/Column/ColumnHeaderView.php b/src/Column/ColumnHeaderView.php
old mode 100644
new mode 100755
diff --git a/src/Column/ColumnInterface.php b/src/Column/ColumnInterface.php
old mode 100644
new mode 100755
index a1c1455f..39fad177
--- a/src/Column/ColumnInterface.php
+++ b/src/Column/ColumnInterface.php
@@ -4,19 +4,38 @@
namespace Kreyu\Bundle\DataTableBundle\Column;
-use Kreyu\Bundle\DataTableBundle\Column\Type\ResolvedColumnTypeInterface;
+use Kreyu\Bundle\DataTableBundle\DataTableInterface;
use Kreyu\Bundle\DataTableBundle\HeaderRowView;
use Kreyu\Bundle\DataTableBundle\ValueRowView;
+use Symfony\Component\PropertyAccess\PropertyPathInterface;
interface ColumnInterface
{
public function getName(): string;
- public function getType(): ResolvedColumnTypeInterface;
+ public function getConfig(): ColumnConfigInterface;
- public function getOptions(): array;
+ public function getDataTable(): DataTableInterface;
+
+ public function setDataTable(DataTableInterface $dataTable): static;
+
+ public function getPropertyPath(): ?PropertyPathInterface;
+
+ public function getSortPropertyPath(): ?PropertyPathInterface;
public function createHeaderView(HeaderRowView $parent = null): ColumnHeaderView;
public function createValueView(ValueRowView $parent = null): ColumnValueView;
+
+ public function createExportHeaderView(HeaderRowView $parent = null): ColumnHeaderView;
+
+ public function createExportValueView(ValueRowView $parent = null): ColumnValueView;
+
+ public function getPriority(): int;
+
+ public function setPriority(int $priority): static;
+
+ public function isVisible(): bool;
+
+ public function setVisible(bool $visible): static;
}
diff --git a/src/Column/ColumnRegistry.php b/src/Column/ColumnRegistry.php
old mode 100644
new mode 100755
index 221e1f36..97792e0e
--- a/src/Column/ColumnRegistry.php
+++ b/src/Column/ColumnRegistry.php
@@ -4,110 +4,33 @@
namespace Kreyu\Bundle\DataTableBundle\Column;
-use Kreyu\Bundle\DataTableBundle\Column\Extension\ColumnTypeExtensionInterface;
+use Kreyu\Bundle\DataTableBundle\AbstractRegistry;
+use Kreyu\Bundle\DataTableBundle\Column\Extension\ColumnExtensionInterface;
use Kreyu\Bundle\DataTableBundle\Column\Type\ColumnTypeInterface;
-use Kreyu\Bundle\DataTableBundle\Column\Type\ResolvedColumnTypeFactoryInterface;
use Kreyu\Bundle\DataTableBundle\Column\Type\ResolvedColumnTypeInterface;
-use Kreyu\Bundle\DataTableBundle\Exception\UnexpectedTypeException;
-class ColumnRegistry implements ColumnRegistryInterface
+/**
+ * @extends AbstractRegistry
+ */
+class ColumnRegistry extends AbstractRegistry implements ColumnRegistryInterface
{
- /**
- * @var array
- */
- private array $types = [];
-
- /**
- * @var array
- */
- private array $resolvedTypes = [];
-
- /**
- * @var array
- */
- private array $checkedTypes = [];
-
- /**
- * @var array
- */
- private array $typeExtensions = [];
-
- /**
- * @param iterable $types
- * @param iterable $typeExtensions
- */
- public function __construct(
- iterable $types,
- iterable $typeExtensions,
- private ResolvedColumnTypeFactoryInterface $resolvedColumnTypeFactory,
- ) {
- foreach ($types as $type) {
- if (!$type instanceof ColumnTypeInterface) {
- throw new UnexpectedTypeException($type, ColumnTypeInterface::class);
- }
-
- $this->types[$type::class] = $type;
- }
-
- foreach ($typeExtensions as $typeExtension) {
- if (!$typeExtension instanceof ColumnTypeExtensionInterface) {
- throw new UnexpectedTypeException($typeExtension, ColumnTypeExtensionInterface::class);
- }
-
- $this->typeExtensions[$typeExtension::class] = $typeExtension;
- }
- }
-
public function getType(string $name): ResolvedColumnTypeInterface
{
- if (!isset($this->resolvedTypes[$name])) {
- if (!isset($this->types[$name])) {
- throw new \InvalidArgumentException(sprintf('Could not load type "%s".', $name));
- }
-
- $this->resolvedTypes[$name] = $this->resolveType($this->types[$name]);
- }
-
- return $this->resolvedTypes[$name];
+ return $this->doGetType($name);
}
- private function resolveType(ColumnTypeInterface $type): ResolvedColumnTypeInterface
+ final protected function getErrorContextName(): string
{
- $fqcn = $type::class;
-
- if (isset($this->checkedTypes[$fqcn])) {
- $types = implode(' > ', array_merge(array_keys($this->checkedTypes), [$fqcn]));
- throw new \LogicException(sprintf('Circular reference detected for column type "%s" (%s).', $fqcn, $types));
- }
-
- $this->checkedTypes[$fqcn] = true;
-
- $typeExtensions = array_filter(
- $this->typeExtensions,
- fn (ColumnTypeExtensionInterface $extension) => $this->isFqcnExtensionEligible($fqcn, $extension),
- );
-
- $parentType = $type->getParent();
-
- try {
- return $this->resolvedColumnTypeFactory->createResolvedType(
- $type,
- $typeExtensions,
- $parentType ? $this->getType($parentType) : null,
- );
- } finally {
- unset($this->checkedTypes[$fqcn]);
- }
+ return 'column';
}
- private function isFqcnExtensionEligible(string $fqcn, ColumnTypeExtensionInterface $extension): bool
+ final protected function getTypeClass(): string
{
- $extendedTypes = $extension::getExtendedTypes();
-
- if ($extendedTypes instanceof \Traversable) {
- $extendedTypes = iterator_to_array($extendedTypes);
- }
+ return ColumnTypeInterface::class;
+ }
- return in_array($fqcn, $extendedTypes);
+ final protected function getExtensionClass(): string
+ {
+ return ColumnExtensionInterface::class;
}
}
diff --git a/src/Column/ColumnRegistryInterface.php b/src/Column/ColumnRegistryInterface.php
old mode 100644
new mode 100755
diff --git a/src/Column/ColumnValueView.php b/src/Column/ColumnValueView.php
old mode 100644
new mode 100755
diff --git a/src/Column/Extension/AbstractColumnExtension.php b/src/Column/Extension/AbstractColumnExtension.php
new file mode 100644
index 00000000..46e03388
--- /dev/null
+++ b/src/Column/Extension/AbstractColumnExtension.php
@@ -0,0 +1,34 @@
+
+ */
+abstract class AbstractColumnExtension extends AbstractExtension implements ColumnExtensionInterface
+{
+ public function getType(string $name): ColumnTypeInterface
+ {
+ return $this->doGetType($name);
+ }
+
+ final protected function getErrorContextName(): string
+ {
+ return 'column';
+ }
+
+ final protected function getTypeClass(): string
+ {
+ return ColumnTypeInterface::class;
+ }
+
+ final protected function getTypeExtensionClass(): string
+ {
+ return ColumnTypeExtensionInterface::class;
+ }
+}
diff --git a/src/Column/Extension/AbstractColumnTypeExtension.php b/src/Column/Extension/AbstractColumnTypeExtension.php
old mode 100644
new mode 100755
index c5a7f00d..73227c1a
--- a/src/Column/Extension/AbstractColumnTypeExtension.php
+++ b/src/Column/Extension/AbstractColumnTypeExtension.php
@@ -4,6 +4,7 @@
namespace Kreyu\Bundle\DataTableBundle\Column\Extension;
+use Kreyu\Bundle\DataTableBundle\Column\ColumnBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Column\ColumnHeaderView;
use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface;
use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView;
@@ -11,6 +12,10 @@
abstract class AbstractColumnTypeExtension implements ColumnTypeExtensionInterface
{
+ public function buildColumn(ColumnBuilderInterface $builder, array $options): void
+ {
+ }
+
public function buildHeaderView(ColumnHeaderView $view, ColumnInterface $column, array $options): void
{
}
@@ -19,6 +24,14 @@ public function buildValueView(ColumnValueView $view, ColumnInterface $column, a
{
}
+ public function buildExportHeaderView(ColumnHeaderView $view, ColumnInterface $column, array $options): void
+ {
+ }
+
+ public function buildExportValueView(ColumnValueView $view, ColumnInterface $column, array $options): void
+ {
+ }
+
public function configureOptions(OptionsResolver $resolver): void
{
}
diff --git a/src/Column/Extension/ColumnExtensionInterface.php b/src/Column/Extension/ColumnExtensionInterface.php
new file mode 100644
index 00000000..3cee99d4
--- /dev/null
+++ b/src/Column/Extension/ColumnExtensionInterface.php
@@ -0,0 +1,18 @@
+doGetType($name);
+ }
+
+ protected function getTypeClass(): string
+ {
+ return ColumnTypeInterface::class;
+ }
+
+ protected function getErrorContextName(): string
+ {
+ return 'column';
+ }
+}
diff --git a/src/Column/Extension/PreloadedColumnExtension.php b/src/Column/Extension/PreloadedColumnExtension.php
new file mode 100644
index 00000000..f7e44bba
--- /dev/null
+++ b/src/Column/Extension/PreloadedColumnExtension.php
@@ -0,0 +1,30 @@
+ $types
+ * @param array> $typeExtensions
+ */
+ public function __construct(
+ private readonly array $types = [],
+ private readonly array $typeExtensions = [],
+ ) {
+ }
+
+ protected function loadTypes(): array
+ {
+ return $this->types;
+ }
+
+ protected function loadTypeExtensions(): array
+ {
+ return $this->typeExtensions;
+ }
+}
diff --git a/src/Column/Type/AbstractColumnType.php b/src/Column/Type/AbstractColumnType.php
old mode 100644
new mode 100755
index 40a3b20c..586b7893
--- a/src/Column/Type/AbstractColumnType.php
+++ b/src/Column/Type/AbstractColumnType.php
@@ -4,6 +4,7 @@
namespace Kreyu\Bundle\DataTableBundle\Column\Type;
+use Kreyu\Bundle\DataTableBundle\Column\ColumnBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Column\ColumnHeaderView;
use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface;
use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView;
@@ -12,6 +13,10 @@
abstract class AbstractColumnType implements ColumnTypeInterface
{
+ public function buildColumn(ColumnBuilderInterface $builder, array $options): void
+ {
+ }
+
public function buildHeaderView(ColumnHeaderView $view, ColumnInterface $column, array $options): void
{
}
@@ -20,6 +25,14 @@ public function buildValueView(ColumnValueView $view, ColumnInterface $column, a
{
}
+ public function buildExportHeaderView(ColumnHeaderView $view, ColumnInterface $column, array $options): void
+ {
+ }
+
+ public function buildExportValueView(ColumnValueView $view, ColumnInterface $column, array $options): void
+ {
+ }
+
public function configureOptions(OptionsResolver $resolver): void
{
}
diff --git a/src/Column/Type/ActionsColumnType.php b/src/Column/Type/ActionsColumnType.php
old mode 100644
new mode 100755
index 98562aea..52ed5b92
--- a/src/Column/Type/ActionsColumnType.php
+++ b/src/Column/Type/ActionsColumnType.php
@@ -16,7 +16,7 @@
class ActionsColumnType extends AbstractColumnType
{
public function __construct(
- private ActionFactoryInterface $actionFactory,
+ private readonly ActionFactoryInterface $actionFactory,
) {
}
diff --git a/src/Column/Type/BooleanColumnType.php b/src/Column/Type/BooleanColumnType.php
old mode 100644
new mode 100755
index 855a0bd8..2e85efab
--- a/src/Column/Type/BooleanColumnType.php
+++ b/src/Column/Type/BooleanColumnType.php
@@ -7,7 +7,7 @@
use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface;
use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView;
use Symfony\Component\OptionsResolver\OptionsResolver;
-use Symfony\Component\Translation\TranslatableMessage;
+use Symfony\Contracts\Translation\TranslatableInterface;
class BooleanColumnType extends AbstractColumnType
{
@@ -27,8 +27,8 @@ public function configureOptions(OptionsResolver $resolver): void
'label_false' => 'No',
'value_translation_domain' => 'KreyuDataTable',
])
- ->setAllowedTypes('label_true', ['string', TranslatableMessage::class])
- ->setAllowedTypes('label_false', ['string', TranslatableMessage::class])
+ ->setAllowedTypes('label_true', ['string', TranslatableInterface::class])
+ ->setAllowedTypes('label_false', ['string', TranslatableInterface::class])
->setInfo('label_true', 'Label displayed when the value equals true.')
->setInfo('label_false', 'Label displayed when the value equals false.')
;
diff --git a/src/Column/Type/CheckboxColumnType.php b/src/Column/Type/CheckboxColumnType.php
old mode 100644
new mode 100755
diff --git a/src/Column/Type/CollectionColumnType.php b/src/Column/Type/CollectionColumnType.php
old mode 100644
new mode 100755
index 6065eeb8..eb793e99
--- a/src/Column/Type/CollectionColumnType.php
+++ b/src/Column/Type/CollectionColumnType.php
@@ -4,42 +4,43 @@
namespace Kreyu\Bundle\DataTableBundle\Column\Type;
-use Kreyu\Bundle\DataTableBundle\Column\ColumnFactoryAwareInterface;
-use Kreyu\Bundle\DataTableBundle\Column\ColumnFactoryAwareTrait;
+use Kreyu\Bundle\DataTableBundle\Column\ColumnBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Column\ColumnFactoryInterface;
use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface;
use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView;
use Symfony\Component\OptionsResolver\OptionsResolver;
-class CollectionColumnType extends AbstractColumnType implements ColumnFactoryAwareInterface
+class CollectionColumnType extends AbstractColumnType
{
- use ColumnFactoryAwareTrait;
+ public function buildColumn(ColumnBuilderInterface $builder, array $options): void
+ {
+ $builder->setAttribute('prototype_factory', $builder->getColumnFactory());
+ }
public function buildValueView(ColumnValueView $view, ColumnInterface $column, array $options): void
{
- $children = [];
-
- foreach ($view->value ?? [] as $index => $data) {
- $child = $this->columnFactory->create(
- name: $column->getName().'__'.($index + 1),
- type: $options['entry_type'],
- options: $options['entry_options'] + [
- 'property_path' => false,
- ],
- );
-
- // Create a virtual row view for the child column.
- $valueRowView = clone $view->parent;
- $valueRowView->origin = $view->parent;
- $valueRowView->index = $index;
- $valueRowView->data = $data;
+ $view->vars = array_replace($view->vars, [
+ 'separator' => $options['separator'],
+ 'children' => $this->createChildrenColumnValueViews($view, $column, $options),
+ ]);
+ }
- $children[] = $child->createValueView($valueRowView);
+ public function buildExportValueView(ColumnValueView $view, ColumnInterface $column, array $options): void
+ {
+ if (!is_array($options['export'])) {
+ $options['export'] = [];
}
- $view->vars = array_replace($view->vars, [
+ $options['export'] += [
+ 'entry_type' => $options['entry_type'],
+ 'entry_options' => $options['entry_options'],
'separator' => $options['separator'],
- 'children' => $children,
- ]);
+ ];
+
+ $view->value = implode($options['export']['separator'], array_map(
+ static fn (ColumnValueView $view) => $view->value,
+ $this->createChildrenColumnValueViews($view, $column, $options['export']),
+ ));
}
public function configureOptions(OptionsResolver $resolver): void
@@ -48,14 +49,33 @@ public function configureOptions(OptionsResolver $resolver): void
->setDefaults([
'entry_type' => TextColumnType::class,
'entry_options' => [],
- 'separator' => ',',
+ 'separator' => ', ',
])
- ->setAllowedTypes('entry_type', ['string'])
- ->setAllowedTypes('entry_options', ['array'])
+ ->setAllowedTypes('entry_type', 'string')
+ ->setAllowedTypes('entry_options', 'array')
->setAllowedTypes('separator', ['null', 'string'])
- ->setInfo('entry_type', 'A fully-qualified class name of the column type to render each entry.')
- ->setInfo('entry_options', 'An array of options passed to the column type.')
- ->setInfo('separator', 'A string used to visually separate each entry.')
;
}
+
+ private function createChildrenColumnValueViews(ColumnValueView $view, ColumnInterface $column, array $options): array
+ {
+ /** @var ColumnFactoryInterface $prototypeFactory */
+ $prototypeFactory = $column->getConfig()->getAttribute('prototype_factory');
+
+ $prototype = $prototypeFactory->createNamed('__name__', $options['entry_type'], $options['entry_options']);
+
+ $children = [];
+
+ foreach ($view->value ?? [] as $index => $data) {
+ // Create a virtual row view for the child column.
+ $valueRowView = clone $view->parent;
+ $valueRowView->origin = $view->parent;
+ $valueRowView->index = $index;
+ $valueRowView->data = $data;
+
+ $children[] = $prototype->createValueView($valueRowView);
+ }
+
+ return $children;
+ }
}
diff --git a/src/Column/Type/ColumnType.php b/src/Column/Type/ColumnType.php
old mode 100644
new mode 100755
index 4f39d5cb..48868c69
--- a/src/Column/Type/ColumnType.php
+++ b/src/Column/Type/ColumnType.php
@@ -4,6 +4,7 @@
namespace Kreyu\Bundle\DataTableBundle\Column\Type;
+use Kreyu\Bundle\DataTableBundle\Column\ColumnBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Column\ColumnHeaderView;
use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface;
use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView;
@@ -12,48 +13,54 @@
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyAccess\PropertyPathInterface;
-use Symfony\Component\Translation\TranslatableMessage;
+use Symfony\Contracts\Translation\TranslatableInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
final class ColumnType implements ColumnTypeInterface
{
+ public function __construct(
+ private ?TranslatorInterface $translator = null,
+ ) {
+ }
+
+ public function buildColumn(ColumnBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->setPropertyPath($options['property_path'] ?: null)
+ ->setSortPropertyPath(is_string($options['sort']) ? $options['sort'] : null)
+ ->setPriority($options['priority'])
+ ->setVisible($options['visible'])
+ ->setPersonalizable($options['personalizable'])
+ ->setSortable(false !== $options['sort'])
+ ->setExportable(false !== $options['export'])
+ ;
+ }
+
public function buildHeaderView(ColumnHeaderView $view, ColumnInterface $column, array $options): void
{
- if (true === $sort = $options['sort']) {
- $sort = $column->getName();
- }
+ $dataTable = $column->getDataTable();
+ $sortColumnData = $dataTable->getSortingData()?->getColumn($column);
- $sortColumnData = $view->parent->parent->vars['sorting_data']?->getColumn($column->getName());
+ $headerRowView = $view->parent;
+ $dataTableView = $headerRowView->parent;
$view->vars = array_replace($view->vars, [
'name' => $column->getName(),
'column' => $view,
- 'row' => $view->parent,
- 'data_table' => $view->parent->parent,
+ 'row' => $headerRowView,
+ 'data_table' => $dataTableView,
'block_prefixes' => $this->getColumnBlockPrefixes($column, $options),
'label' => $options['label'] ?? StringUtil::camelToSentence($column->getName()),
+ 'translation_domain' => $options['header_translation_domain'] ?? $dataTableView->vars['translation_domain'] ?? null,
'translation_parameters' => $options['header_translation_parameters'],
- 'translation_domain' => $options['header_translation_domain'] ?? $view->parent->parent->vars['translation_domain'] ?? null,
- 'sort_parameter_name' => $view->parent->parent->vars['sort_parameter_name'],
+ 'sort_parameter_name' => $dataTable->getConfig()->getSortParameterName(),
'attr' => $options['header_attr'],
'sorted' => null !== $sortColumnData,
- 'sort_field' => $sort,
+ 'sort_field' => $column->getSortPropertyPath(),
'sort_direction' => $sortColumnData?->getDirection(),
- 'export' => false,
+ 'sortable' => $column->getConfig()->isSortable(),
+ 'export' => $column->getConfig()->isExportable(),
]);
-
- if (true === $export = $options['export']) {
- $export = [];
- }
-
- if (false !== $export) {
- $export = array_merge($options, [
- 'label' => $export['label'] ?? $view->vars['label'],
- 'translation_parameters' => $export['translation_parameters'] ?? $view->vars['translation_parameters'] ?? null,
- 'translation_domain' => $export['translation_domain'] ?? $view->vars['translation_domain'] ?? null,
- ], $export);
- }
-
- $view->vars['export'] = $export;
}
public function buildValueView(ColumnValueView $view, ColumnInterface $column, array $options): void
@@ -76,32 +83,84 @@ public function buildValueView(ColumnValueView $view, ColumnInterface $column, a
'block_prefixes' => $this->getColumnBlockPrefixes($column, $options),
'data' => $view->data,
'value' => $view->value,
- 'translation_domain' => $options['value_translation_domain'] ?? $view->parent->vars['translation_domain'] ?? null,
+ 'translation_domain' => $options['value_translation_domain'] ?? $view->parent->parent->vars['translation_domain'] ?? null,
'translation_parameters' => $options['value_translation_parameters'] ?? [],
'attr' => $attr,
]);
+ }
- if (true === $export = $options['export']) {
- $export = [];
+ public function buildExportHeaderView(ColumnHeaderView $view, ColumnInterface $column, array $options): void
+ {
+ if (false === $options['export']) {
+ return;
+ }
+
+ if (true === $options['export']) {
+ $options['export'] = [];
+ }
+
+ $options['export'] += [
+ 'getter' => $options['getter'],
+ 'property_path' => $options['property_path'],
+ 'formatter' => $options['formatter'],
+ ];
+
+ $label = $options['label'] ?? StringUtil::camelToSentence($column->getName());
+
+ if ($this->translator) {
+ $translationDomain = $options['export']['header_translation_domain']
+ ?? $options['header_translation_domain']
+ ?? $view->parent->parent->vars['translation_domain']
+ ?? false;
+
+ if ($translationDomain) {
+ $label = $this->translator->trans(
+ id: $label,
+ parameters: $options['header_translation_parameters'],
+ domain: $translationDomain,
+ );
+ }
}
- if (false !== $export) {
- $export = array_merge($options, $export);
+ $view->vars['label'] = $label;
+ }
- $export = $column->getType()->getOptionsResolver()->resolve($export);
+ public function buildExportValueView(ColumnValueView $view, ColumnInterface $column, array $options): void
+ {
+ if (false === $options['export']) {
+ return;
+ }
- $normData = $this->getNormDataFromRowData($rowData, $column, $export);
- $viewData = $this->getViewDataFromNormData($normData, $column, $export);
+ if (true === $options['export']) {
+ $options['export'] = [];
+ }
- $export = array_merge([
- 'data' => $normData,
- 'value' => $viewData,
- 'label' => $export['label'] ?? null,
- 'translation_domain' => $export['translation_domain'] ?? $view->vars['translation_domain'],
- ], $export);
+ $options['export'] += [
+ 'getter' => $options['getter'],
+ 'property_path' => $options['property_path'],
+ 'property_accessor' => $options['property_accessor'],
+ 'formatter' => $options['formatter'],
+ ];
+
+ $normData = $this->getNormDataFromRowData($view->parent->data, $column, $options['export']);
+ $viewData = $this->getViewDataFromNormData($normData, $column, $options['export']);
+
+ if ($this->translator && is_string($viewData)) {
+ $translationDomain = $options['export']['value_translation_domain']
+ ?? $options['value_translation_domain']
+ ?? $view->parent->parent->vars['translation_domain']
+ ?? false;
+
+ if ($translationDomain) {
+ $viewData = $this->translator->trans(
+ id: $viewData,
+ parameters: $options['value_translation_parameters'],
+ domain: $translationDomain,
+ );
+ }
}
- $view->vars['export'] = $export;
+ $view->value = $viewData;
}
public function configureOptions(OptionsResolver $resolver): void
@@ -123,37 +182,28 @@ public function configureOptions(OptionsResolver $resolver): void
'getter' => null,
'header_attr' => [],
'value_attr' => [],
+ 'priority' => 0,
+ 'visible' => true,
+ 'personalizable' => true,
])
- ->setAllowedTypes('label', ['null', 'string', TranslatableMessage::class])
+ ->setAllowedTypes('label', ['null', 'string', TranslatableInterface::class])
->setAllowedTypes('header_translation_domain', ['null', 'bool', 'string'])
->setAllowedTypes('header_translation_parameters', ['null', 'array'])
->setAllowedTypes('value_translation_domain', ['null', 'bool', 'string'])
- ->setAllowedTypes('value_translation_parameters', ['array'])
+ ->setAllowedTypes('value_translation_parameters', 'array')
->setAllowedTypes('block_name', ['null', 'string'])
->setAllowedTypes('block_prefix', ['null', 'string'])
->setAllowedTypes('sort', ['bool', 'string'])
->setAllowedTypes('export', ['bool', 'array'])
->setAllowedTypes('formatter', ['null', 'callable'])
->setAllowedTypes('property_path', ['null', 'bool', 'string', PropertyPathInterface::class])
- ->setAllowedTypes('property_accessor', [PropertyAccessorInterface::class])
+ ->setAllowedTypes('property_accessor', PropertyAccessorInterface::class)
->setAllowedTypes('getter', ['null', 'callable'])
- ->setAllowedTypes('header_attr', ['array'])
+ ->setAllowedTypes('header_attr', 'array')
->setAllowedTypes('value_attr', ['array', 'callable'])
- ->setInfo('label', 'A user-friendly label that describes a column.')
- ->setInfo('header_translation_domain', 'Translation domain used to translate the column header.')
- ->setInfo('header_translation_parameters', 'Parameters used within the column header translation.')
- ->setInfo('value_translation_domain', 'Translation domain used to translate the column value.')
- ->setInfo('value_translation_parameters', 'Parameters used within the column value translation.')
- ->setInfo('block_name', 'Name of the block that renders the column.')
- ->setInfo('block_prefix', 'A custom prefix of the block name that renders the column.')
- ->setInfo('sort', 'Determines whether the column can be sorted (and optionally on what path).')
- ->setInfo('export', 'Determines whether the column can be exported (and optionally with custom options).')
- ->setInfo('formatter', 'A formatter used to format the column norm data to the view data.')
- ->setInfo('property_path', 'Property path used to retrieve the column norm data from the row data.')
- ->setInfo('property_accessor', 'An instance of property accessor used to retrieve column norm data from the row data.')
- ->setInfo('getter', 'A callable data accessor used to retrieve column norm data from the row data manually, instead of property accessor.')
- ->setInfo('header_attr', 'An array of attributes (e.g. HTML attributes) passed to the header view.')
- ->setInfo('value_attr', 'An array of attributes (e.g. HTML attributes) passed to the column value view.')
+ ->setAllowedTypes('priority', 'int')
+ ->setAllowedTypes('visible', 'bool')
+ ->setAllowedTypes('personalizable', 'bool')
;
}
@@ -186,7 +236,7 @@ private function getNormDataFromRowData(mixed $data, ColumnInterface $column, ar
$propertyPath = $options['property_path'] ?? $column->getName();
- if (is_string($propertyPath) && (is_array($data) || is_object($data))) {
+ if ((is_string($propertyPath) || $propertyPath instanceof PropertyPathInterface) && (is_array($data) || is_object($data))) {
return $options['property_accessor']->getValue($data, $propertyPath);
}
@@ -219,7 +269,7 @@ private function getViewDataFromNormData(mixed $data, ColumnInterface $column, a
*/
private function getColumnBlockPrefixes(ColumnInterface $column, array $options): array
{
- $type = $column->getType();
+ $type = $column->getConfig()->getType();
$blockPrefixes = [
$type->getBlockPrefix(),
diff --git a/src/Column/Type/ColumnTypeInterface.php b/src/Column/Type/ColumnTypeInterface.php
old mode 100644
new mode 100755
index 697c581a..1e7e4d25
--- a/src/Column/Type/ColumnTypeInterface.php
+++ b/src/Column/Type/ColumnTypeInterface.php
@@ -4,6 +4,7 @@
namespace Kreyu\Bundle\DataTableBundle\Column\Type;
+use Kreyu\Bundle\DataTableBundle\Column\ColumnBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Column\ColumnHeaderView;
use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface;
use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView;
@@ -11,10 +12,16 @@
interface ColumnTypeInterface
{
+ public function buildColumn(ColumnBuilderInterface $builder, array $options): void;
+
public function buildHeaderView(ColumnHeaderView $view, ColumnInterface $column, array $options): void;
public function buildValueView(ColumnValueView $view, ColumnInterface $column, array $options): void;
+ public function buildExportHeaderView(ColumnHeaderView $view, ColumnInterface $column, array $options): void;
+
+ public function buildExportValueView(ColumnValueView $view, ColumnInterface $column, array $options): void;
+
public function configureOptions(OptionsResolver $resolver): void;
public function getBlockPrefix(): string;
diff --git a/src/Column/Type/DateColumnType.php b/src/Column/Type/DateColumnType.php
new file mode 100755
index 00000000..65c42ced
--- /dev/null
+++ b/src/Column/Type/DateColumnType.php
@@ -0,0 +1,20 @@
+setDefault('format', 'd.m.Y');
+ }
+
+ public function getParent(): ?string
+ {
+ return DateTimeColumnType::class;
+ }
+}
diff --git a/src/Column/Type/DatePeriodColumnType.php b/src/Column/Type/DatePeriodColumnType.php
old mode 100644
new mode 100755
diff --git a/src/Column/Type/DateTimeColumnType.php b/src/Column/Type/DateTimeColumnType.php
old mode 100644
new mode 100755
index 7a256287..eeb17a29
--- a/src/Column/Type/DateTimeColumnType.php
+++ b/src/Column/Type/DateTimeColumnType.php
@@ -6,6 +6,7 @@
use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface;
use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView;
+use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class DateTimeColumnType extends AbstractColumnType
@@ -25,6 +26,25 @@ public function configureOptions(OptionsResolver $resolver): void
'format' => 'd.m.Y H:i:s',
'timezone' => null,
])
+ ->setNormalizer('export', function (Options $options, $value) {
+ if (true === $value) {
+ $value = [];
+ }
+
+ if (is_array($value)) {
+ $value += [
+ 'formatter' => static function (mixed $dateTime, ColumnInterface $column): string {
+ if ($dateTime instanceof \DateTimeInterface) {
+ return $dateTime->format($column->getConfig()->getOption('format'));
+ }
+
+ return '';
+ },
+ ];
+ }
+
+ return $value;
+ })
->setAllowedTypes('format', ['string'])
->setAllowedTypes('timezone', ['null', 'string'])
->setInfo('format', 'A date time string format, supported by the PHP date() function.')
diff --git a/src/Column/Type/FormColumnType.php b/src/Column/Type/FormColumnType.php
old mode 100644
new mode 100755
diff --git a/src/Column/Type/LinkColumnType.php b/src/Column/Type/LinkColumnType.php
old mode 100644
new mode 100755
diff --git a/src/Column/Type/MoneyColumnType.php b/src/Column/Type/MoneyColumnType.php
old mode 100644
new mode 100755
diff --git a/src/Column/Type/NumberColumnType.php b/src/Column/Type/NumberColumnType.php
old mode 100644
new mode 100755
diff --git a/src/Column/Type/ResolvedColumnType.php b/src/Column/Type/ResolvedColumnType.php
old mode 100644
new mode 100755
index 73334356..ee2058b2
--- a/src/Column/Type/ResolvedColumnType.php
+++ b/src/Column/Type/ResolvedColumnType.php
@@ -4,12 +4,16 @@
namespace Kreyu\Bundle\DataTableBundle\Column\Type;
+use Kreyu\Bundle\DataTableBundle\Column\ColumnBuilder;
+use Kreyu\Bundle\DataTableBundle\Column\ColumnBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Column\ColumnFactoryInterface;
use Kreyu\Bundle\DataTableBundle\Column\ColumnHeaderView;
use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface;
use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView;
use Kreyu\Bundle\DataTableBundle\Column\Extension\ColumnTypeExtensionInterface;
use Kreyu\Bundle\DataTableBundle\HeaderRowView;
use Kreyu\Bundle\DataTableBundle\ValueRowView;
+use Symfony\Component\OptionsResolver\Exception\ExceptionInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ResolvedColumnType implements ResolvedColumnTypeInterface
@@ -20,9 +24,9 @@ class ResolvedColumnType implements ResolvedColumnTypeInterface
* @param array $typeExtensions
*/
public function __construct(
- private ColumnTypeInterface $innerType,
- private array $typeExtensions = [],
- private ?ResolvedColumnTypeInterface $parent = null,
+ private readonly ColumnTypeInterface $innerType,
+ private readonly array $typeExtensions = [],
+ private readonly ?ResolvedColumnTypeInterface $parent = null,
) {
}
@@ -46,6 +50,23 @@ public function getTypeExtensions(): array
return $this->typeExtensions;
}
+ /**
+ * @throws ExceptionInterface
+ */
+ public function createBuilder(ColumnFactoryInterface $factory, string $name, array $options): ColumnBuilderInterface
+ {
+ try {
+ $options = $this->getOptionsResolver()->resolve($options);
+ } catch (ExceptionInterface $exception) {
+ throw new $exception(sprintf('An error has occurred resolving the options of the column "%s": ', get_debug_type($this->getInnerType())).$exception->getMessage(), $exception->getCode(), $exception);
+ }
+
+ $builder = new ColumnBuilder($name, $this, $options);
+ $builder->setColumnFactory($factory);
+
+ return $builder;
+ }
+
public function createHeaderView(ColumnInterface $column, HeaderRowView $parent = null): ColumnHeaderView
{
return new ColumnHeaderView($parent);
@@ -56,6 +77,27 @@ public function createValueView(ColumnInterface $column, ValueRowView $parent =
return new ColumnValueView($parent);
}
+ public function createExportHeaderView(ColumnInterface $column, HeaderRowView $parent = null): ColumnHeaderView
+ {
+ return new ColumnHeaderView($parent);
+ }
+
+ public function createExportValueView(ColumnInterface $column, ValueRowView $parent = null): ColumnValueView
+ {
+ return new ColumnValueView($parent);
+ }
+
+ public function buildColumn(ColumnBuilderInterface $builder, array $options): void
+ {
+ $this->parent?->buildColumn($builder, $options);
+
+ $this->innerType->buildColumn($builder, $options);
+
+ foreach ($this->typeExtensions as $extension) {
+ $extension->buildColumn($builder, $options);
+ }
+ }
+
public function buildHeaderView(ColumnHeaderView $view, ColumnInterface $column, array $options): void
{
$this->parent?->buildHeaderView($view, $column, $options);
@@ -78,6 +120,28 @@ public function buildValueView(ColumnValueView $view, ColumnInterface $column, a
}
}
+ public function buildExportHeaderView(ColumnHeaderView $view, ColumnInterface $column, array $options): void
+ {
+ $this->parent?->buildExportHeaderView($view, $column, $options);
+
+ $this->innerType->buildExportHeaderView($view, $column, $options);
+
+ foreach ($this->typeExtensions as $typeExtension) {
+ $typeExtension->buildExportHeaderView($view, $column, $options);
+ }
+ }
+
+ public function buildExportValueView(ColumnValueView $view, ColumnInterface $column, array $options): void
+ {
+ $this->parent?->buildExportValueView($view, $column, $options);
+
+ $this->innerType->buildExportValueView($view, $column, $options);
+
+ foreach ($this->typeExtensions as $typeExtension) {
+ $typeExtension->buildExportValueView($view, $column, $options);
+ }
+ }
+
public function getOptionsResolver(): OptionsResolver
{
if (!isset($this->optionsResolver)) {
diff --git a/src/Column/Type/ResolvedColumnTypeFactory.php b/src/Column/Type/ResolvedColumnTypeFactory.php
old mode 100644
new mode 100755
index bc56d327..637452bb
--- a/src/Column/Type/ResolvedColumnTypeFactory.php
+++ b/src/Column/Type/ResolvedColumnTypeFactory.php
@@ -6,7 +6,7 @@
class ResolvedColumnTypeFactory implements ResolvedColumnTypeFactoryInterface
{
- public function createResolvedType(ColumnTypeInterface $type, array $typeExtensions, ResolvedColumnTypeInterface $parent = null): ResolvedColumnTypeInterface
+ public function createResolvedType(ColumnTypeInterface $type, array $typeExtensions = [], ResolvedColumnTypeInterface $parent = null): ResolvedColumnTypeInterface
{
return new ResolvedColumnType($type, $typeExtensions, $parent);
}
diff --git a/src/Column/Type/ResolvedColumnTypeFactoryInterface.php b/src/Column/Type/ResolvedColumnTypeFactoryInterface.php
old mode 100644
new mode 100755
index b62bb759..c8f896b7
--- a/src/Column/Type/ResolvedColumnTypeFactoryInterface.php
+++ b/src/Column/Type/ResolvedColumnTypeFactoryInterface.php
@@ -4,7 +4,12 @@
namespace Kreyu\Bundle\DataTableBundle\Column\Type;
+use Kreyu\Bundle\DataTableBundle\Column\Extension\ColumnTypeExtensionInterface;
+
interface ResolvedColumnTypeFactoryInterface
{
- public function createResolvedType(ColumnTypeInterface $type, array $typeExtensions, ResolvedColumnTypeInterface $parent = null): ResolvedColumnTypeInterface;
+ /**
+ * @param array $typeExtensions
+ */
+ public function createResolvedType(ColumnTypeInterface $type, array $typeExtensions = [], ResolvedColumnTypeInterface $parent = null): ResolvedColumnTypeInterface;
}
diff --git a/src/Column/Type/ResolvedColumnTypeInterface.php b/src/Column/Type/ResolvedColumnTypeInterface.php
old mode 100644
new mode 100755
index 6c8e3aad..45dacfd5
--- a/src/Column/Type/ResolvedColumnTypeInterface.php
+++ b/src/Column/Type/ResolvedColumnTypeInterface.php
@@ -4,6 +4,8 @@
namespace Kreyu\Bundle\DataTableBundle\Column\Type;
+use Kreyu\Bundle\DataTableBundle\Column\ColumnBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Column\ColumnFactoryInterface;
use Kreyu\Bundle\DataTableBundle\Column\ColumnHeaderView;
use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface;
use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView;
@@ -25,13 +27,25 @@ public function getInnerType(): ColumnTypeInterface;
*/
public function getTypeExtensions(): array;
+ public function createBuilder(ColumnFactoryInterface $factory, string $name, array $options): ColumnBuilderInterface;
+
public function createHeaderView(ColumnInterface $column, HeaderRowView $parent = null): ColumnHeaderView;
public function createValueView(ColumnInterface $column, ValueRowView $parent = null): ColumnValueView;
+ public function createExportHeaderView(ColumnInterface $column, HeaderRowView $parent = null): ColumnHeaderView;
+
+ public function createExportValueView(ColumnInterface $column, ValueRowView $parent = null): ColumnValueView;
+
+ public function buildColumn(ColumnBuilderInterface $builder, array $options): void;
+
public function buildHeaderView(ColumnHeaderView $view, ColumnInterface $column, array $options): void;
public function buildValueView(ColumnValueView $view, ColumnInterface $column, array $options): void;
+ public function buildExportHeaderView(ColumnHeaderView $view, ColumnInterface $column, array $options): void;
+
+ public function buildExportValueView(ColumnValueView $view, ColumnInterface $column, array $options): void;
+
public function getOptionsResolver(): OptionsResolver;
}
diff --git a/src/Column/Type/TemplateColumnType.php b/src/Column/Type/TemplateColumnType.php
old mode 100644
new mode 100755
index d220dfaa..39a51bc0
--- a/src/Column/Type/TemplateColumnType.php
+++ b/src/Column/Type/TemplateColumnType.php
@@ -13,11 +13,11 @@ class TemplateColumnType extends AbstractColumnType
public function buildValueView(ColumnValueView $view, ColumnInterface $column, array $options): void
{
if (is_callable($templatePath = $options['template_path'])) {
- $templatePath = $templatePath($view->vars['data']);
+ $templatePath = $templatePath($view->data, $column);
}
if (is_callable($templateVars = $options['template_vars'])) {
- $templateVars = $templateVars($view->vars['data']);
+ $templateVars = $templateVars($view->data, $column);
}
$view->vars = array_merge($view->vars, [
diff --git a/src/Column/Type/TextColumnType.php b/src/Column/Type/TextColumnType.php
old mode 100644
new mode 100755
diff --git a/src/DataTable.php b/src/DataTable.php
old mode 100644
new mode 100755
index 08510421..db327025
--- a/src/DataTable.php
+++ b/src/DataTable.php
@@ -6,26 +6,51 @@
use Kreyu\Bundle\DataTableBundle\Action\ActionContext;
use Kreyu\Bundle\DataTableBundle\Action\ActionInterface;
+use Kreyu\Bundle\DataTableBundle\Action\Type\ActionType;
+use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface;
+use Kreyu\Bundle\DataTableBundle\Column\Type\ColumnType;
+use Kreyu\Bundle\DataTableBundle\Event\DataTableEvent;
+use Kreyu\Bundle\DataTableBundle\Event\DataTableEvents;
+use Kreyu\Bundle\DataTableBundle\Event\DataTableExportEvent;
+use Kreyu\Bundle\DataTableBundle\Event\DataTableFiltrationEvent;
+use Kreyu\Bundle\DataTableBundle\Event\DataTablePaginationEvent;
+use Kreyu\Bundle\DataTableBundle\Event\DataTablePersonalizationEvent;
+use Kreyu\Bundle\DataTableBundle\Event\DataTableSortingEvent;
use Kreyu\Bundle\DataTableBundle\Exception\OutOfBoundsException;
+use Kreyu\Bundle\DataTableBundle\Exception\RuntimeException;
use Kreyu\Bundle\DataTableBundle\Exporter\ExportData;
+use Kreyu\Bundle\DataTableBundle\Exporter\ExporterInterface;
use Kreyu\Bundle\DataTableBundle\Exporter\ExportFile;
use Kreyu\Bundle\DataTableBundle\Exporter\ExportStrategy;
use Kreyu\Bundle\DataTableBundle\Exporter\Form\Type\ExportDataType;
+use Kreyu\Bundle\DataTableBundle\Exporter\Type\ExporterType;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FiltrationData;
use Kreyu\Bundle\DataTableBundle\Filter\Form\Type\FiltrationDataType;
+use Kreyu\Bundle\DataTableBundle\Filter\Type\FilterType;
use Kreyu\Bundle\DataTableBundle\Pagination\PaginationData;
use Kreyu\Bundle\DataTableBundle\Pagination\PaginationInterface;
use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface;
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceContext;
use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectInterface;
use Kreyu\Bundle\DataTableBundle\Personalization\Form\Type\PersonalizationDataType;
use Kreyu\Bundle\DataTableBundle\Personalization\PersonalizationData;
use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
-use Kreyu\Bundle\DataTableBundle\Sorting\SortingColumnData;
use Kreyu\Bundle\DataTableBundle\Sorting\SortingData;
use Symfony\Component\Form\FormBuilderInterface;
class DataTable implements DataTableInterface
{
+ /**
+ * @var array
+ */
+ private array $columns = [];
+
+ /**
+ * @var array
+ */
+ private array $filters = [];
+
/**
* @var array
*/
@@ -41,6 +66,11 @@ class DataTable implements DataTableInterface
*/
private array $rowActions = [];
+ /**
+ * @var array
+ */
+ private array $exporters = [];
+
/**
* The sorting data currently applied to the data table.
*/
@@ -74,18 +104,51 @@ class DataTable implements DataTableInterface
/**
* The copy of a query used to retrieve the data, without any filters applied.
*/
- private ProxyQueryInterface $nonFilteredQuery;
+ private ProxyQueryInterface $originalQuery;
+
+ private bool $initialized = false;
public function __construct(
private ProxyQueryInterface $query,
- private DataTableConfigInterface $config,
+ private /* readonly */ DataTableConfigInterface $config,
) {
- $this->nonFilteredQuery = clone $this->query;
+ $this->originalQuery = clone $this->query;
}
public function __clone(): void
{
$this->config = clone $this->config;
+ $this->query = clone $this->query;
+ }
+
+ public function initialize(): void
+ {
+ if ($this->initialized) {
+ return;
+ }
+
+ if ($paginationData = $this->getInitialPaginationData()) {
+ $this->paginate($paginationData, false);
+ }
+
+ if ($sortingData = $this->getInitialSortingData()) {
+ $this->sort($sortingData, false);
+ }
+
+ if ($filtrationData = $this->getInitialFiltrationData()) {
+ $this->filter($filtrationData, false);
+ }
+
+ if ($personalizationData = $this->getInitialPersonalizationData()) {
+ $this->personalize($personalizationData, false);
+ }
+
+ $this->initialized = true;
+ }
+
+ public function getName(): string
+ {
+ return $this->config->getName();
}
public function getQuery(): ProxyQueryInterface
@@ -98,6 +161,120 @@ public function getConfig(): DataTableConfigInterface
return $this->config;
}
+ public function getColumns(): array
+ {
+ $columns = $this->columns;
+
+ uasort($columns, static function (ColumnInterface $columnA, ColumnInterface $columnB) {
+ return $columnA->getPriority() < $columnB->getPriority();
+ });
+
+ return $columns;
+ }
+
+ public function getVisibleColumns(): array
+ {
+ return array_filter(
+ $this->getColumns(),
+ static fn (ColumnInterface $column) => $column->isVisible(),
+ );
+ }
+
+ public function getHiddenColumns(): array
+ {
+ return array_filter(
+ $this->getColumns(),
+ static fn (ColumnInterface $column) => !$column->isVisible(),
+ );
+ }
+
+ public function getExportableColumns(): array
+ {
+ return array_filter(
+ $this->getVisibleColumns(),
+ static fn (ColumnInterface $column) => $column->getConfig()->isExportable(),
+ );
+ }
+
+ public function getColumn(string $name): ColumnInterface
+ {
+ if (isset($this->columns[$name])) {
+ return $this->columns[$name];
+ }
+
+ throw new OutOfBoundsException(sprintf('Column "%s" does not exist.', $name));
+ }
+
+ public function hasColumn(string $name): bool
+ {
+ return array_key_exists($name, $this->columns);
+ }
+
+ public function addColumn(ColumnInterface|string $column, string $type = ColumnType::class, array $options = []): static
+ {
+ if (is_string($column)) {
+ $column = $this->config->getColumnFactory()
+ ->createNamedBuilder($column, $type, $options)
+ ->getColumn()
+ ;
+ }
+
+ $this->columns[$column->getName()] = $column;
+
+ $column->setDataTable($this);
+
+ return $this;
+ }
+
+ public function removeColumn(string $name): static
+ {
+ unset($this->columns[$name]);
+
+ return $this;
+ }
+
+ public function getFilters(): array
+ {
+ return $this->filters;
+ }
+
+ public function getFilter(string $name): FilterInterface
+ {
+ if (isset($this->filters[$name])) {
+ return $this->filters[$name];
+ }
+
+ throw new OutOfBoundsException(sprintf('Filter "%s" does not exist.', $name));
+ }
+
+ public function hasFilter(string $name): bool
+ {
+ return array_key_exists($name, $this->filters);
+ }
+
+ public function addFilter(FilterInterface|string $filter, string $type = FilterType::class, array $options = []): static
+ {
+ if (is_string($filter)) {
+ $filter = $this->config->getFilterFactory()
+ ->createNamedBuilder($filter, $type, $options)
+ ->getFilter()
+ ;
+ }
+
+ $this->filters[$filter->getName()] = $filter;
+
+ $filter->setDataTable($this);
+
+ return $this;
+ }
+
+ public function removeFilter(string $name): static
+ {
+ unset($this->filters[$name]);
+
+ return $this;
+ }
+
public function getActions(): array
{
return $this->actions;
@@ -117,10 +294,10 @@ public function hasAction(string $name): bool
return array_key_exists($name, $this->actions);
}
- public function addAction(ActionInterface|string $action, string $type = null, array $options = []): static
+ public function addAction(ActionInterface|string $action, string $type = ActionType::class, array $options = []): static
{
if (is_string($action)) {
- $builder = $this->getConfig()->getActionFactory()->createNamedBuilder($action, $type, $options);
+ $builder = $this->config->getActionFactory()->createNamedBuilder($action, $type, $options);
$builder->setContext(ActionContext::Global);
$action = $builder->getAction();
@@ -159,10 +336,10 @@ public function hasBatchAction(string $name): bool
return array_key_exists($name, $this->batchActions);
}
- public function addBatchAction(ActionInterface|string $action, string $type = null, array $options = []): static
+ public function addBatchAction(ActionInterface|string $action, string $type = ActionType::class, array $options = []): static
{
if (is_string($action)) {
- $builder = $this->getConfig()->getActionFactory()->createNamedBuilder($action, $type, $options);
+ $builder = $this->config->getActionFactory()->createNamedBuilder($action, $type, $options);
$builder->setContext(ActionContext::Batch);
$action = $builder->getAction();
@@ -201,10 +378,10 @@ public function hasRowAction(string $name): bool
return array_key_exists($name, $this->rowActions);
}
- public function addRowAction(ActionInterface|string $action, string $type = null, array $options = []): static
+ public function addRowAction(ActionInterface|string $action, string $type = ActionType::class, array $options = []): static
{
if (is_string($action)) {
- $builder = $this->getConfig()->getActionFactory()->createNamedBuilder($action, $type, $options);
+ $builder = $this->config->getActionFactory()->createNamedBuilder($action, $type, $options);
$builder->setContext(ActionContext::Row);
$action = $builder->getAction();
@@ -224,78 +401,117 @@ public function removeRowAction(string $name): static
return $this;
}
- public function paginate(PaginationData $data): void
+ public function getExporters(): array
{
- if (!$this->config->isPaginationEnabled()) {
- return;
- }
+ return $this->exporters;
+ }
- $this->query->paginate($data);
+ public function getExporter(string $name): ExporterInterface
+ {
+ if (isset($this->exporters[$name])) {
+ return $this->exporters[$name];
+ }
- $this->nonFilteredQuery = $this->query;
+ throw new OutOfBoundsException(sprintf('Exporter "%s" does not exist.', $name));
+ }
- $this->paginationData = $data;
+ public function hasExporter(string $name): bool
+ {
+ return array_key_exists($name, $this->exporters);
+ }
- if ($this->config->isPaginationPersistenceEnabled()) {
- $this->setPersistenceData('pagination', $data);
+ public function addExporter(ExporterInterface|string $exporter, string $type = ExporterType::class, array $options = []): static
+ {
+ if (is_string($exporter)) {
+ $exporter = $this->config->getExporterFactory()
+ ->createNamedBuilder($exporter, $type, $options)
+ ->getExporter()
+ ;
}
- $this->pagination = null;
+ $this->exporters[$exporter->getName()] = $exporter;
+
+ $exporter->setDataTable($this);
+
+ return $this;
}
- public function sort(SortingData $data): void
+ public function removeExporter(string $name): static
{
- if (!$this->config->isSortingEnabled()) {
+ unset($this->exporters[$name]);
+
+ return $this;
+ }
+
+ public function paginate(PaginationData $data, bool $persistence = true): void
+ {
+ if (!$this->config->isPaginationEnabled()) {
return;
}
- $sortingDataFiltered = new SortingData();
+ $this->dispatch(DataTableEvents::PRE_PAGINATE, $event = new DataTablePaginationEvent($this, $data));
- foreach ($data->getColumns() as $sortingColumnData) {
- try {
- $column = $this->getConfig()->getColumn($sortingColumnData->getName());
- } catch (\Throwable) {
- continue;
- }
+ $data = $event->getPaginationData();
+
+ $this->query->paginate($data);
- $sortField = $column->getOptions()['sort'];
+ $this->originalQuery = $this->query;
- if (false === $sortField) {
- continue;
- }
+ if ($persistence && $this->config->isPaginationPersistenceEnabled()) {
+ $this->setPersistenceData(PersistenceContext::Pagination, $data);
+ }
- if (true === $sortField) {
- $sortField = $column->getOptions()['property_path'] ?: $column->getName();
- }
+ $this->setPaginationData($data);
+ $this->resetPagination();
- $sortingDataFiltered->addColumn($column->getName(), SortingColumnData::fromArray([
- 'name' => $sortField,
- 'direction' => $sortingColumnData->getDirection(),
- ]));
+ $this->dispatch(DataTableEvents::POST_PAGINATE, new DataTablePaginationEvent($this, $data));
+ }
+
+ public function sort(SortingData $data, bool $persistence = true): void
+ {
+ if (!$this->config->isSortingEnabled()) {
+ return;
}
- $this->query->sort($sortingDataFiltered);
+ $this->dispatch(DataTableEvents::PRE_SORT, $event = new DataTableSortingEvent($this, $data));
- $this->nonFilteredQuery = $this->query;
+ $data = $event->getSortingData();
- $this->sortingData = $data;
+ $columns = $this->getColumns();
- if ($this->config->isSortingPersistenceEnabled()) {
- $this->setPersistenceData('sorting', $data);
+ $data->removeRedundantColumns($columns);
+ $data->ensureValidPropertyPaths($columns);
+
+ $this->query->sort($data);
+
+ $this->originalQuery = $this->query;
+
+ if ($persistence && $this->config->isSortingPersistenceEnabled()) {
+ $this->setPersistenceData(PersistenceContext::Sorting, $data);
}
- $this->pagination = null;
+ $this->setSortingData($data);
+ $this->resetPagination();
+
+ $this->dispatch(DataTableEvents::POST_SORT, new DataTableSortingEvent($this, $data));
}
- public function filter(FiltrationData $data): void
+ public function filter(FiltrationData $data, bool $persistence = true): void
{
if (!$this->config->isFiltrationEnabled()) {
return;
}
- $this->query = clone $this->nonFilteredQuery;
+ $this->query = clone $this->originalQuery;
+
+ $this->dispatch(DataTableEvents::PRE_FILTER, $event = new DataTableFiltrationEvent($this, $data));
+
+ $data = $event->getFiltrationData();
- $filters = $this->config->getFilters();
+ $filters = $this->getFilters();
+
+ $data->appendMissingFilters($filters);
+ $data->removeRedundantFilters($filters);
foreach ($filters as $filter) {
$filterData = $data->getFilterData($filter->getName());
@@ -305,109 +521,166 @@ public function filter(FiltrationData $data): void
}
}
- $filters = $this->config->getFilters();
-
- $data->appendMissingFilters($filters);
- $data->removeRedundantFilters($filters);
-
- $this->filtrationData = $data;
-
- if ($this->config->isFiltrationPersistenceEnabled()) {
- $this->setPersistenceData('filtration', $data);
+ if ($persistence && $this->config->isFiltrationPersistenceEnabled()) {
+ $this->setPersistenceData(PersistenceContext::Filtration, $data);
}
- $this->pagination = null;
+ $this->setFiltrationData($data);
+ $this->resetPagination();
+
+ $this->dispatch(DataTableEvents::POST_FILTER, new DataTableFiltrationEvent($this, $data));
}
- public function personalize(PersonalizationData $data): void
+ public function personalize(PersonalizationData $data, bool $persistence = true): void
{
if (!$this->config->isPersonalizationEnabled()) {
return;
}
- $columns = $this->config->getColumns();
+ $this->dispatch(DataTableEvents::PRE_PERSONALIZE, $event = new DataTablePersonalizationEvent($this, $data));
+
+ $data = $event->getPersonalizationData();
+
+ $columns = $this->getColumns();
$data->addMissingColumns($columns);
$data->removeRedundantColumns($columns);
- if ($this->config->isPersonalizationPersistenceEnabled()) {
- $this->setPersistenceData('personalization', $data);
+ if ($persistence && $this->config->isPersonalizationPersistenceEnabled()) {
+ $this->setPersistenceData(PersistenceContext::Personalization, $data);
}
- $this->personalizationData = $data;
+ $this->setPersonalizationData($data);
+
+ $data->apply($this->getColumns());
+
+ $this->dispatch(DataTableEvents::POST_PERSONALIZE, new DataTablePersonalizationEvent($this, $data));
}
public function export(ExportData $data = null): ExportFile
{
if (!$this->config->isExportingEnabled()) {
- throw new \RuntimeException('The data table requested to export has exporting feature disabled.');
+ throw new RuntimeException('The data table has exporting feature disabled.');
}
- $data ??= $this->exportData ?? $this->getConfig()->getDefaultExportData() ?? ExportData::fromDataTable($this);
+ $dataTable = clone $this;
+
+ $data ??= $this->exportData ?? $this->config->getDefaultExportData() ?? ExportData::fromDataTable($this);
- $this->exportData = $data;
+ // TODO: Remove "getNonDeprecatedCase()" call once the deprecated strategies are removed.
+ $data->strategy = $data->strategy->getNonDeprecatedCase();
- $dataTable = clone $this;
+ $this->dispatch(DataTableEvents::PRE_EXPORT, $event = new DataTableExportEvent($dataTable, $data));
- // TODO: This should be done in a better way...
- if ($dataTable->config instanceof DataTableConfigBuilderInterface) {
- $dataTable->config->setPaginationPersistenceEnabled(false);
- $dataTable->config->setPersonalizationPersistenceEnabled(false);
- }
+ $data = $event->getExportData();
- if (ExportStrategy::INCLUDE_ALL === $data->strategy) {
- $dataTable->paginate(new PaginationData(perPage: null));
+ if (ExportStrategy::IncludeAll === $data->strategy) {
+ $dataTable->getQuery()->paginate(new PaginationData(perPage: null));
}
if (!$data->includePersonalization) {
- $dataTable->personalize(PersonalizationData::fromDataTable($this));
+ $dataTable->resetPersonalization();
+ }
+
+ if (null === $data->exporter) {
+ $exporter = $this->exporters[array_key_first($this->exporters)];
+ } else {
+ $exporter = $this->getExporter($data->exporter);
+ }
+
+ return $exporter->export($dataTable->createExportView(), $data->filename);
+ }
+
+ public function getItems(): iterable
+ {
+ if ($this->getConfig()->isPaginationEnabled()) {
+ return $this->getPagination()->getItems();
}
- return $data->exporter->export($dataTable->createView(), $data->filename);
+ return $this->query->getItems();
}
public function getPagination(): PaginationInterface
{
+ if (!$this->config->isPaginationEnabled()) {
+ throw new RuntimeException('The data table has pagination feature disabled.');
+ }
+
return $this->pagination ??= $this->query->getPagination();
}
- public function getSortingData(): SortingData
+ public function getSortingData(): ?SortingData
{
return $this->sortingData;
}
- public function getPaginationData(): PaginationData
+ public function setSortingData(?SortingData $sortingData): static
+ {
+ $this->sortingData = $sortingData;
+
+ return $this;
+ }
+
+ public function getPaginationData(): ?PaginationData
{
return $this->paginationData;
}
+ public function setPaginationData(?PaginationData $paginationData): static
+ {
+ $this->paginationData = $paginationData;
+
+ return $this;
+ }
+
public function getFiltrationData(): ?FiltrationData
{
return $this->filtrationData;
}
- public function getPersonalizationData(): PersonalizationData
+ public function setFiltrationData(?FiltrationData $filtrationData): static
+ {
+ $this->filtrationData = $filtrationData;
+
+ return $this;
+ }
+
+ public function getPersonalizationData(): ?PersonalizationData
{
return $this->personalizationData;
}
+ public function setPersonalizationData(?PersonalizationData $personalizationData): static
+ {
+ $this->personalizationData = $personalizationData;
+
+ return $this;
+ }
+
public function getExportData(): ?ExportData
{
return $this->exportData;
}
+ public function setExportData(?ExportData $exportData): static
+ {
+ $this->exportData = $exportData;
+
+ return $this;
+ }
+
public function createFiltrationFormBuilder(DataTableView $view = null): FormBuilderInterface
{
if (!$this->config->isFiltrationEnabled()) {
- throw new \RuntimeException('The data table has filtration feature disabled.');
+ throw new RuntimeException('The data table has filtration feature disabled.');
}
- if (null === $this->config->getFiltrationFormFactory()) {
- throw new \RuntimeException('The data table has no configured filtration form factory.');
+ if (null === $formFactory = $this->config->getFiltrationFormFactory()) {
+ throw new RuntimeException('The data table has no configured filtration form factory.');
}
- return $this->config->getFiltrationFormFactory()->createNamedBuilder(
- name: $this->getConfig()->getFiltrationParameterName(),
+ return $formFactory->createNamedBuilder(
+ name: $this->config->getFiltrationParameterName(),
type: FiltrationDataType::class,
options: [
'data_table' => $this,
@@ -419,15 +692,15 @@ public function createFiltrationFormBuilder(DataTableView $view = null): FormBui
public function createPersonalizationFormBuilder(DataTableView $view = null): FormBuilderInterface
{
if (!$this->config->isPersonalizationEnabled()) {
- throw new \RuntimeException('The data table has personalization feature disabled.');
+ throw new RuntimeException('The data table has personalization feature disabled.');
}
- if (null === $this->config->getPersonalizationFormFactory()) {
- throw new \RuntimeException('The data table has no configured personalization form factory.');
+ if (null === $formFactory = $this->config->getPersonalizationFormFactory()) {
+ throw new RuntimeException('The data table has no configured personalization form factory.');
}
- return $this->config->getFiltrationFormFactory()->createNamedBuilder(
- name: $this->getConfig()->getPersonalizationParameterName(),
+ return $formFactory->createNamedBuilder(
+ name: $this->config->getPersonalizationParameterName(),
type: PersonalizationDataType::class,
options: [
'data_table_view' => $view,
@@ -435,21 +708,28 @@ public function createPersonalizationFormBuilder(DataTableView $view = null): Fo
);
}
- public function createExportFormBuilder(): FormBuilderInterface
+ public function createExportFormBuilder(DataTableView $view = null): FormBuilderInterface
{
if (!$this->config->isExportingEnabled()) {
- throw new \RuntimeException('The data table has export feature disabled.');
+ throw new RuntimeException('The data table has export feature disabled.');
}
- if (null === $this->config->getExportFormFactory()) {
- throw new \RuntimeException('The data table has no configured export form factory.');
+ if (null === $formFactory = $this->config->getExportFormFactory()) {
+ throw new RuntimeException('The data table has no configured export form factory.');
}
- return $this->config->getExportFormFactory()->createNamedBuilder(
- name: $this->getConfig()->getExportParameterName(),
+ $data = $this->config->getDefaultExportData() ?? ExportData::fromDataTable($this);
+
+ if (null !== $data) {
+ $data->filename ??= $this->getName();
+ }
+
+ return $formFactory->createNamedBuilder(
+ name: $this->config->getExportParameterName(),
type: ExportDataType::class,
+ data: $data,
options: [
- 'exporters' => $this->config->getExporters(),
+ 'exporters' => $this->getExporters(),
],
);
}
@@ -467,7 +747,7 @@ public function hasActiveFilters(): bool
public function handleRequest(mixed $request): void
{
if (null === $requestHandler = $this->config->getRequestHandler()) {
- throw new \RuntimeException('The "handleRequest" method cannot be used on data tables without configured request handler.');
+ throw new RuntimeException('The "handleRequest" method cannot be used on data tables without configured request handler.');
}
$requestHandler->handle($this, $request);
@@ -475,10 +755,6 @@ public function handleRequest(mixed $request): void
public function createView(): DataTableView
{
- if (empty($this->config->getColumns())) {
- throw new \LogicException('The data table has no configured columns.');
- }
-
$type = $this->config->getType();
$options = $this->config->getOptions();
@@ -489,25 +765,32 @@ public function createView(): DataTableView
return $view;
}
- public function initialize(): void
+ public function createExportView(): DataTableView
{
- if ($paginationData = $this->getInitialPaginationData()) {
- $this->paginate($paginationData);
- }
+ $type = $this->config->getType();
+ $options = $this->config->getOptions();
- if ($sortingData = $this->getInitialSortingData()) {
- $this->sort($sortingData);
- }
+ $view = $type->createExportView($this);
- if ($filtrationData = $this->getInitialFiltrationData()) {
- $this->filter($filtrationData);
- }
+ $type->buildExportView($view, $this, $options);
- if ($personalizationData = $this->getInitialPersonalizationData()) {
- $this->personalize($personalizationData);
+ return $view;
+ }
+
+ private function dispatch(string $eventName, DataTableEvent $event): void
+ {
+ $dispatcher = $this->config->getEventDispatcher();
+
+ if ($dispatcher->hasListeners($eventName)) {
+ $dispatcher->dispatch($event, $eventName);
}
}
+ private function resetPagination(): void
+ {
+ $this->pagination = null;
+ }
+
private function getInitialPaginationData(): ?PaginationData
{
if (!$this->config->isPaginationEnabled()) {
@@ -517,7 +800,7 @@ private function getInitialPaginationData(): ?PaginationData
$data = null;
if ($this->config->isPaginationPersistenceEnabled()) {
- $data = $this->getPersistenceData('pagination');
+ $data = $this->getPersistenceData(PersistenceContext::Pagination);
}
$data ??= $this->config->getDefaultPaginationData();
@@ -536,7 +819,7 @@ private function getInitialSortingData(): ?SortingData
$data = null;
if ($this->config->isSortingPersistenceEnabled()) {
- $data = $this->getPersistenceData('sorting');
+ $data = $this->getPersistenceData(PersistenceContext::Sorting);
}
$data ??= $this->config->getDefaultSortingData();
@@ -555,14 +838,14 @@ private function getInitialFiltrationData(): ?FiltrationData
$data = null;
if ($this->config->isFiltrationPersistenceEnabled()) {
- $data = $this->getPersistenceData('filtration');
+ $data = $this->getPersistenceData(PersistenceContext::Filtration);
}
$data ??= $this->config->getDefaultFiltrationData();
$data ??= FiltrationData::fromDataTable($this);
- $data->appendMissingFilters($this->getConfig()->getFilters());
+ $data->appendMissingFilters($this->getFilters());
return $data;
}
@@ -576,7 +859,7 @@ private function getInitialPersonalizationData(): ?PersonalizationData
$data = null;
if ($this->config->isPersonalizationPersistenceEnabled()) {
- $data = $this->getPersistenceData('personalization');
+ $data = $this->getPersistenceData(PersistenceContext::Personalization);
}
$data ??= $this->config->getDefaultPersonalizationData();
@@ -586,21 +869,20 @@ private function getInitialPersonalizationData(): ?PersonalizationData
return $data;
}
- private function isPersistenceEnabled(string $context): bool
+ private function isPersistenceEnabled(PersistenceContext $context): bool
{
return match ($context) {
- 'sorting' => $this->config->isSortingPersistenceEnabled(),
- 'pagination' => $this->config->isPaginationPersistenceEnabled(),
- 'filtration' => $this->config->isFiltrationPersistenceEnabled(),
- 'personalization' => $this->config->isPersonalizationPersistenceEnabled(),
- default => throw new \RuntimeException('Given persistence context is not supported.'),
+ PersistenceContext::Sorting => $this->config->isSortingPersistenceEnabled(),
+ PersistenceContext::Pagination => $this->config->isPaginationPersistenceEnabled(),
+ PersistenceContext::Filtration => $this->config->isFiltrationPersistenceEnabled(),
+ PersistenceContext::Personalization => $this->config->isPersonalizationPersistenceEnabled(),
};
}
- private function getPersistenceData(string $context): mixed
+ private function getPersistenceData(PersistenceContext $context): mixed
{
if (!$this->isPersistenceEnabled($context)) {
- throw new \RuntimeException(sprintf('The data table has %s persistence disabled.', $context));
+ throw new RuntimeException(sprintf('The data table has %s persistence disabled.', $context->value));
}
$persistenceAdapter = $this->getPersistenceAdapter($context);
@@ -609,10 +891,10 @@ private function getPersistenceData(string $context): mixed
return $persistenceAdapter->read($this, $persistenceSubject);
}
- private function setPersistenceData(string $context, mixed $data): void
+ private function setPersistenceData(PersistenceContext $context, mixed $data): void
{
if (!$this->isPersistenceEnabled($context)) {
- throw new \RuntimeException(sprintf('The data table has %s persistence disabled.', $context));
+ throw new RuntimeException(sprintf('The data table has %s persistence disabled.', $context->value));
}
$persistenceAdapter = $this->getPersistenceAdapter($context);
@@ -621,37 +903,50 @@ private function setPersistenceData(string $context, mixed $data): void
$persistenceAdapter->write($this, $persistenceSubject, $data);
}
- private function getPersistenceAdapter(string $context): PersistenceAdapterInterface
+ private function getPersistenceAdapter(PersistenceContext $context): PersistenceAdapterInterface
{
$adapter = match ($context) {
- 'sorting' => $this->config->getSortingPersistenceAdapter(),
- 'pagination' => $this->config->getPaginationPersistenceAdapter(),
- 'filtration' => $this->config->getFiltrationPersistenceAdapter(),
- 'personalization' => $this->config->getPersonalizationPersistenceAdapter(),
- default => throw new \RuntimeException('Given persistence context is not supported.'),
+ PersistenceContext::Sorting => $this->config->getSortingPersistenceAdapter(),
+ PersistenceContext::Pagination => $this->config->getPaginationPersistenceAdapter(),
+ PersistenceContext::Filtration => $this->config->getFiltrationPersistenceAdapter(),
+ PersistenceContext::Personalization => $this->config->getPersonalizationPersistenceAdapter(),
};
if (null === $adapter) {
- throw new \RuntimeException(sprintf('The data table is configured to use %s persistence, but does not have an adapter.', $context));
+ throw new RuntimeException(sprintf('The data table is configured to use %s persistence, but does not have an adapter.', $context->value));
}
return $adapter;
}
- private function getPersistenceSubject(string $context): PersistenceSubjectInterface
+ /**
+ * @throws Persistence\PersistenceSubjectNotFoundException
+ */
+ private function getPersistenceSubject(PersistenceContext $context): PersistenceSubjectInterface
{
- $subject = match ($context) {
- 'sorting' => $this->config->getSortingPersistenceSubject(),
- 'pagination' => $this->config->getPaginationPersistenceSubject(),
- 'filtration' => $this->config->getFiltrationPersistenceSubject(),
- 'personalization' => $this->config->getPersonalizationPersistenceSubject(),
- default => throw new \RuntimeException('Given persistence context is not supported.'),
+ $provider = match ($context) {
+ PersistenceContext::Sorting => $this->config->getSortingPersistenceSubjectProvider(),
+ PersistenceContext::Pagination => $this->config->getPaginationPersistenceSubjectProvider(),
+ PersistenceContext::Filtration => $this->config->getFiltrationPersistenceSubjectProvider(),
+ PersistenceContext::Personalization => $this->config->getPersonalizationPersistenceSubjectProvider(),
};
- if (null === $subject) {
- throw new \RuntimeException(sprintf('The data table is configured to use %s persistence, but does not have a subject.', $context));
+ if (null === $provider) {
+ throw new RuntimeException(sprintf('The data table is configured to use %s persistence, but does not have a subject provider.', $context->value));
}
- return $subject;
+ return $provider->provide();
+ }
+
+ private function resetPersonalization(): void
+ {
+ $this->personalizationData = null;
+
+ foreach ($this->columns as $column) {
+ $column
+ ->setPriority($column->getConfig()->getOption('priority'))
+ ->setVisible($column->getConfig()->getOption('visible'))
+ ;
+ }
}
}
diff --git a/src/DataTableBuilder.php b/src/DataTableBuilder.php
old mode 100644
new mode 100755
index c5e35198..ec85c3fa
--- a/src/DataTableBuilder.php
+++ b/src/DataTableBuilder.php
@@ -6,87 +6,67 @@
use Kreyu\Bundle\DataTableBundle\Action\ActionBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Action\ActionContext;
-use Kreyu\Bundle\DataTableBundle\Action\ActionFactoryInterface;
use Kreyu\Bundle\DataTableBundle\Action\Type\ActionTypeInterface;
-use Kreyu\Bundle\DataTableBundle\Column\ColumnFactoryInterface;
-use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface;
+use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType;
+use Kreyu\Bundle\DataTableBundle\Column\ColumnBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Column\Type\ActionsColumnType;
use Kreyu\Bundle\DataTableBundle\Column\Type\CheckboxColumnType;
+use Kreyu\Bundle\DataTableBundle\Column\Type\ColumnTypeInterface;
+use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType;
+use Kreyu\Bundle\DataTableBundle\Exception\BadMethodCallException;
use Kreyu\Bundle\DataTableBundle\Exception\InvalidArgumentException;
-use Kreyu\Bundle\DataTableBundle\Exporter\ExportData;
-use Kreyu\Bundle\DataTableBundle\Exporter\ExporterFactoryInterface;
-use Kreyu\Bundle\DataTableBundle\Exporter\ExporterInterface;
-use Kreyu\Bundle\DataTableBundle\Filter\FilterFactoryInterface;
-use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
-use Kreyu\Bundle\DataTableBundle\Filter\FiltrationData;
-use Kreyu\Bundle\DataTableBundle\Pagination\PaginationData;
-use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface;
-use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectInterface;
-use Kreyu\Bundle\DataTableBundle\Personalization\PersonalizationData;
+use Kreyu\Bundle\DataTableBundle\Exporter\ExporterBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Exporter\Type\ExporterType;
+use Kreyu\Bundle\DataTableBundle\Exporter\Type\ExporterTypeInterface;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Filter\Type\FilterType;
+use Kreyu\Bundle\DataTableBundle\Filter\Type\FilterTypeInterface;
+use Kreyu\Bundle\DataTableBundle\Filter\Type\SearchFilterType;
use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
-use Kreyu\Bundle\DataTableBundle\Request\RequestHandlerInterface;
-use Kreyu\Bundle\DataTableBundle\Sorting\SortingData;
use Kreyu\Bundle\DataTableBundle\Type\ResolvedDataTableTypeInterface;
-use Symfony\Component\Form\FormFactoryInterface;
-use Symfony\Component\Translation\TranslatableMessage;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
-class DataTableBuilder implements DataTableBuilderInterface
+class DataTableBuilder extends DataTableConfigBuilder implements DataTableBuilderInterface
{
/**
- * Name of the data table, used to differentiate multiple data tables on the same page.
- */
- private string $name;
-
- /**
- * Resolved type class, containing instructions on how to build a data table.
- */
- private ResolvedDataTableTypeInterface $type;
-
- /**
- * Query used to retrieve and manipulate source of the data table.
- */
- private null|ProxyQueryInterface $query;
-
- /**
- * Stores an array of options, used to configure a builder behavior.
- */
- private array $options;
-
- /**
- * Stores an array of themes to used to render the data table.
+ * The column builders defined for the data table.
*
- * @var array
+ * @var array
*/
- private array $themes;
+ private array $columns = [];
/**
- * User-friendly title used to describe a data table.
+ * The data of columns that haven't been converted to column builders yet.
+ *
+ * @var array, 1: array}>
*/
- private null|string|TranslatableMessage $title = null;
+ private array $unresolvedColumns = [];
/**
- * Stores an array of parameters used in translation of the user-friendly title.
+ * The column builders defined for the data table.
+ *
+ * @var array
*/
- private array $titleTranslationParameters = [];
+ private array $filters = [];
/**
- * Domain name used in the translation of the data table elements.
+ * The data of filters that haven't been converted to filter builders yet.
+ *
+ * @var array, 1: array}>
*/
- private null|false|string $translationDomain = null;
+ private array $unresolvedFilters = [];
/**
- * Stores an array of columns, used to display the data table to the user.
- *
- * @var array
+ * The search handler used to filter the data table using a single search query.
*/
- private array $columns = [];
+ private ?\Closure $searchHandler = null;
/**
- * Stores an array of filters, used to build and handle the filtering feature.
- *
- * @var array
+ * Determines whether the builder should automatically add {@see SearchFilterType}
+ * when a search handler is defined in {@see DataTableBuilder::$searchHandler}.
*/
- private array $filters = [];
+ private bool $autoAddingSearchFilter = true;
/**
* The action builders defined for the data table.
@@ -143,182 +123,27 @@ class DataTableBuilder implements DataTableBuilderInterface
private bool $autoAddingActionsColumn = true;
/**
- * Stores an array of exporters, used to output data to various file types.
+ * The exporter builders defined for the data table.
*
- * @var array
+ * @var array
*/
private array $exporters = [];
/**
- * Factory used to create proper column models.
- */
- private ColumnFactoryInterface $columnFactory;
-
- /**
- * Factory used to create proper filter models.
- */
- private FilterFactoryInterface $filterFactory;
-
- /**
- * Factory used to create proper action models.
- */
- private ActionFactoryInterface $actionFactory;
-
- /**
- * Factory used to create proper exporter models.
- */
- private ExporterFactoryInterface $exporterFactory;
-
- /**
- * Determines whether the data table exporting feature is enabled.
- */
- private bool $exportingEnabled = true;
-
- /**
- * Form factory used to create an export form.
- */
- private null|FormFactoryInterface $exportFormFactory = null;
-
- /**
- * Default export data, which is applied to the data table if no data is given by the user.
- */
- private null|ExportData $defaultExportData = null;
-
- /**
- * Determines whether the data table personalization feature is enabled.
- */
- private bool $personalizationEnabled = true;
-
- /**
- * Determines whether the data table personalization persistence feature is enabled.
- */
- private bool $personalizationPersistenceEnabled = true;
-
- /**
- * Persistence adapter used to read/write personalization feature data.
- */
- private null|PersistenceAdapterInterface $personalizationPersistenceAdapter = null;
-
- /**
- * Subject (e.g. logged-in user) used to associate with the personalization persistence feature data.
- */
- private null|PersistenceSubjectInterface $personalizationPersistenceSubject = null;
-
- /**
- * Form factory used to create a personalization form.
- */
- private null|FormFactoryInterface $personalizationFormFactory = null;
-
- /**
- * Default personalization data, which is applied to the data table if no data is given by the user.
- */
- private null|PersonalizationData $defaultPersonalizationData = null;
-
- /**
- * Determines whether the data table filtration feature is enabled.
- */
- private bool $filtrationEnabled = true;
-
- /**
- * Determines whether the data table filtration persistence feature is enabled.
- */
- private bool $filtrationPersistenceEnabled = true;
-
- /**
- * Persistence adapter used to read/write filtration feature data.
- */
- private null|PersistenceAdapterInterface $filtrationPersistenceAdapter = null;
-
- /**
- * Subject (e.g. logged-in user) used to associate with the filtration persistence feature data.
- */
- private null|PersistenceSubjectInterface $filtrationPersistenceSubject = null;
-
- /**
- * Form factory used to create a filtration form.
- */
- private null|FormFactoryInterface $filtrationFormFactory = null;
-
- /**
- * Default filtration data, which is applied to the data table if no data is given by the user.
- */
- private null|FiltrationData $defaultFiltrationData = null;
-
- /**
- * Determines whether the data table sorting feature is enabled.
- */
- private bool $sortingEnabled = true;
-
- /**
- * Determines whether the data table sorting persistence feature is enabled.
- */
- private bool $sortingPersistenceEnabled = true;
-
- /**
- * Persistence adapter used to read/write sorting feature data.
- */
- private null|PersistenceAdapterInterface $sortingPersistenceAdapter = null;
-
- /**
- * Subject (e.g. logged-in user) used to associate with the sorting persistence feature data.
- */
- private null|PersistenceSubjectInterface $sortingPersistenceSubject = null;
-
- /**
- * Default sorting data, which is applied to the data table if no data is given by the user.
- */
- private null|SortingData $defaultSortingData = null;
-
- /**
- * Determines whether the data table pagination feature is enabled.
- */
- private bool $paginationEnabled = true;
-
- /**
- * Determines whether the data table pagination persistence feature is enabled.
- */
- private bool $paginationPersistenceEnabled = true;
-
- /**
- * Persistence adapter used to read/write pagination feature data.
- */
- private null|PersistenceAdapterInterface $paginationPersistenceAdapter = null;
-
- /**
- * Subject (e.g. logged-in user) used to associate with the pagination persistence feature data.
- */
- private null|PersistenceSubjectInterface $paginationPersistenceSubject = null;
-
- /**
- * Default pagination data, which is applied to the data table if no data is given by the user.
- */
- private null|PaginationData $defaultPaginationData = null;
-
- /**
- * Request handler class used to automatically apply data from request to the data table.
- */
- private null|RequestHandlerInterface $requestHandler = null;
-
- /**
- * Represents HTML attributes rendered on header row.
- */
- private array $headerRowAttributes = [];
-
- /**
- * Represents HTML attributes rendered on each value row.
- */
- private array $valueRowAttributes = [];
-
- /**
- * Determines whether the builder is locked, therefore no setters can be called.
+ * The data of exporters that haven't been converted to exporter builders yet.
+ *
+ * @var array, 1: array}>
*/
- private bool $locked = false;
+ private array $unresolvedExporters = [];
- public function __construct(string $name, ProxyQueryInterface $query = null, array $options = [])
- {
- $this->name = $name;
- $this->query = $query;
- $this->options = $options;
+ public function __construct(
+ string $name,
+ ResolvedDataTableTypeInterface $type,
+ private ?ProxyQueryInterface $query = null,
+ EventDispatcherInterface $dispatcher = new EventDispatcher(),
+ array $options = [],
+ ) {
+ parent::__construct($name, $type, $dispatcher, $options);
}
public function __clone(): void
@@ -326,173 +151,210 @@ public function __clone(): void
$this->query = clone $this->query;
}
- public function getName(): string
+ public function getQuery(): ?ProxyQueryInterface
{
- return $this->name;
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ return $this->query;
}
- public function setName(string $name): static
+ public function setQuery(?ProxyQueryInterface $query): static
{
- $this->name = $name;
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->query = $query;
return $this;
}
- public function getType(): ResolvedDataTableTypeInterface
+ public function getColumns(): array
{
- return $this->type;
- }
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
- public function setType(ResolvedDataTableTypeInterface $type): static
- {
- $this->type = $type;
+ $this->resolveColumns();
- return $this;
+ return $this->columns;
}
- public function getQuery(): ?ProxyQueryInterface
+ public function getColumn(string $name): ColumnBuilderInterface
{
- return $this->query;
- }
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
- public function setQuery(?ProxyQueryInterface $query): static
- {
- $this->query = $query;
+ if (isset($this->unresolvedColumns[$name])) {
+ return $this->resolveColumn($name);
+ }
- return $this;
- }
+ if (isset($this->columns[$name])) {
+ return $this->columns[$name];
+ }
- public function getOptions(): array
- {
- return $this->options;
+ throw new InvalidArgumentException(sprintf('The column with the name "%s" does not exist.', $name));
}
- public function setOptions(array $options): static
+ public function addColumn(ColumnBuilderInterface|string $column, string $type = null, array $options = []): static
{
- $this->options = $options;
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
- return $this;
- }
+ if ($column instanceof ColumnBuilderInterface) {
+ $this->columns[$column->getName()] = $column;
- public function getThemes(): array
- {
- return $this->themes;
- }
+ unset($this->unresolvedColumns[$column->getName()]);
- public function setThemes(array $themes): static
- {
- $this->themes = $themes;
+ return $this;
+ }
+
+ $this->columns[$column] = null;
+ $this->unresolvedColumns[$column] = [$type ?? TextColumnType::class, $options];
return $this;
}
- public function addTheme(string $theme): static
+ public function hasColumn(string $name): bool
{
- $this->themes[] = $theme;
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
- return $this;
+ return array_key_exists($name, $this->columns)
+ || array_key_exists($name, $this->unresolvedColumns);
}
- public function removeTheme(string $theme): static
+ public function removeColumn(string $name): static
{
- if (false !== $key = array_search($theme, $this->themes, true)) {
- unset($this->themes[$key]);
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
}
+ unset($this->unresolvedColumns[$name], $this->columns[$name]);
+
return $this;
}
- public function getTitle(): null|string|TranslatableMessage
+ public function getFilters(): array
{
- return $this->title;
- }
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
- public function setTitle(null|string|TranslatableMessage $title): static
- {
- $this->title = $title;
+ $this->resolveFilters();
- return $this;
+ return $this->filters;
}
- public function getTitleTranslationParameters(): array
+ public function getFilter(string $name): FilterBuilderInterface
{
- return $this->titleTranslationParameters;
- }
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
- public function setTitleTranslationParameters(array $titleTranslationParameters): static
- {
- $this->titleTranslationParameters = $titleTranslationParameters;
+ if (isset($this->unresolvedFilters[$name])) {
+ return $this->resolveFilter($name);
+ }
- return $this;
- }
+ if (isset($this->filters[$name])) {
+ return $this->filters[$name];
+ }
- public function getTranslationDomain(): null|bool|string
- {
- return $this->translationDomain;
+ throw new InvalidArgumentException(sprintf('The filter with the name "%s" does not exist.', $name));
}
- public function setTranslationDomain(null|bool|string $translationDomain): static
+ public function addFilter(string|FilterBuilderInterface $filter, string $type = null, array $options = []): static
{
- $this->translationDomain = $translationDomain;
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ if ($filter instanceof FilterBuilderInterface) {
+ $this->filters[$filter->getName()] = $filter;
+
+ unset($this->unresolvedFilters[$filter->getName()]);
+
+ return $this;
+ }
+
+ $this->filters[$filter] = null;
+ $this->unresolvedFilters[$filter] = [$type ?? FilterType::class, $options];
return $this;
}
- public function getColumns(): array
+ public function hasFilter(string $name): bool
{
- return $this->columns;
- }
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
- public function getColumn(string $name): ColumnInterface
- {
- return $this->columns[$name] ?? throw new \InvalidArgumentException("Column \"$name\" does not exist");
+ return array_key_exists($name, $this->filters)
+ || array_key_exists($name, $this->unresolvedFilters);
}
- public function hasColumn(string $name): bool
+ public function removeFilter(string $name): static
{
- return array_key_exists($name, $this->columns);
- }
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
- public function addColumn(string $name, string $type, array $options = []): static
- {
- $this->columns[$name] = $this->getColumnFactory()->create($name, $type, $options);
+ unset($this->unresolvedFilters[$name], $this->filters[$name]);
return $this;
}
- public function removeColumn(string $name): static
+ public function getSearchHandler(): ?callable
{
- unset($this->columns[$name]);
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
- return $this;
+ return $this->searchHandler;
}
- public function getFilters(): array
+ public function setSearchHandler(?callable $searchHandler): static
{
- return $this->filters;
- }
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
- public function getFilter(string $name): FilterInterface
- {
- return $this->filters[$name] ?? throw new \InvalidArgumentException("Filter \"$name\" does not exist");
+ $this->searchHandler = $searchHandler;
+
+ return $this;
}
- public function addFilter(string $name, string $type, array $options = []): static
+ public function isAutoAddingSearchFilter(): bool
{
- $this->filters[$name] = $this->getFilterFactory()->create($name, $type, $options);
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
- return $this;
+ return $this->autoAddingSearchFilter;
}
- public function removeFilter(string $name): static
+ public function setAutoAddingSearchFilter(bool $autoAddingSearchFilter): static
{
- unset($this->filters[$name]);
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->autoAddingSearchFilter = $autoAddingSearchFilter;
return $this;
}
public function getActions(): array
{
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
$this->resolveActions();
return $this->actions;
@@ -500,6 +362,10 @@ public function getActions(): array
public function getAction(string $name): ActionBuilderInterface
{
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
if (isset($this->unresolvedActions[$name])) {
return $this->resolveAction($name);
}
@@ -513,6 +379,10 @@ public function getAction(string $name): ActionBuilderInterface
public function addAction(string|ActionBuilderInterface $action, string $type = null, array $options = []): static
{
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
if ($action instanceof ActionBuilderInterface) {
$this->actions[$action->getName()] = $action;
@@ -522,19 +392,27 @@ public function addAction(string|ActionBuilderInterface $action, string $type =
}
$this->actions[$action] = null;
- $this->unresolvedActions[$action] = [$type, $options];
+ $this->unresolvedActions[$action] = [$type ?? ButtonActionType::class, $options];
return $this;
}
public function hasAction(string $name): bool
{
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
return array_key_exists($name, $this->actions)
|| array_key_exists($name, $this->unresolvedActions);
}
public function removeAction(string $name): static
{
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
unset($this->unresolvedActions[$name], $this->actions[$name]);
return $this;
@@ -542,6 +420,10 @@ public function removeAction(string $name): static
public function getBatchActions(): array
{
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
$this->resolveBatchActions();
return $this->batchActions;
@@ -549,6 +431,10 @@ public function getBatchActions(): array
public function getBatchAction(string $name): ActionBuilderInterface
{
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
if (isset($this->unresolvedBatchActions[$name])) {
return $this->resolveBatchAction($name);
}
@@ -562,12 +448,20 @@ public function getBatchAction(string $name): ActionBuilderInterface
public function hasBatchAction(string $name): bool
{
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
return array_key_exists($name, $this->batchActions)
|| array_key_exists($name, $this->unresolvedBatchActions);
}
public function addBatchAction(string|ActionBuilderInterface $action, string $type = null, array $options = []): static
{
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
if ($action instanceof ActionBuilderInterface) {
$this->batchActions[$action->getName()] = $action;
@@ -577,13 +471,17 @@ public function addBatchAction(string|ActionBuilderInterface $action, string $ty
}
$this->batchActions[$action] = null;
- $this->unresolvedBatchActions[$action] = [$type, $options];
+ $this->unresolvedBatchActions[$action] = [$type ?? ButtonActionType::class, $options];
return $this;
}
public function removeBatchAction(string $name): static
{
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
unset($this->unresolvedActions[$name], $this->batchActions[$name]);
return $this;
@@ -591,11 +489,19 @@ public function removeBatchAction(string $name): static
public function isAutoAddingBatchCheckboxColumn(): bool
{
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
return $this->autoAddingBatchCheckboxColumn;
}
public function setAutoAddingBatchCheckboxColumn(bool $autoAddingBatchCheckboxColumn): static
{
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
$this->autoAddingBatchCheckboxColumn = $autoAddingBatchCheckboxColumn;
return $this;
@@ -603,6 +509,10 @@ public function setAutoAddingBatchCheckboxColumn(bool $autoAddingBatchCheckboxCo
public function getRowActions(): array
{
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
$this->resolveRowActions();
return $this->rowActions;
@@ -610,6 +520,10 @@ public function getRowActions(): array
public function getRowAction(string $name): ActionBuilderInterface
{
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
if (isset($this->unresolvedRowActions[$name])) {
return $this->resolveRowAction($name);
}
@@ -623,12 +537,20 @@ public function getRowAction(string $name): ActionBuilderInterface
public function hasRowAction(string $name): bool
{
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
return array_key_exists($name, $this->rowActions)
|| array_key_exists($name, $this->unresolvedRowActions);
}
public function addRowAction(string|ActionBuilderInterface $action, string $type = null, array $options = []): static
{
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
if ($action instanceof ActionBuilderInterface) {
$this->rowActions[$action->getName()] = $action;
@@ -638,13 +560,17 @@ public function addRowAction(string|ActionBuilderInterface $action, string $type
}
$this->rowActions[$action] = null;
- $this->unresolvedRowActions[$action] = [$type, $options];
+ $this->unresolvedRowActions[$action] = [$type ?? ButtonActionType::class, $options];
return $this;
}
public function removeRowAction(string $name): static
{
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
unset($this->unresolvedActions[$name], $this->rowActions[$name]);
return $this;
@@ -652,11 +578,19 @@ public function removeRowAction(string $name): static
public function isAutoAddingActionsColumn(): bool
{
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
return $this->autoAddingActionsColumn;
}
public function setAutoAddingActionsColumn(bool $autoAddingActionsColumn): static
{
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
$this->autoAddingActionsColumn = $autoAddingActionsColumn;
return $this;
@@ -664,488 +598,170 @@ public function setAutoAddingActionsColumn(bool $autoAddingActionsColumn): stati
public function getExporters(): array
{
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->resolveExporters();
+
return $this->exporters;
}
- public function getExporter(string $name): ExporterInterface
+ public function getExporter(string $name): ExporterBuilderInterface
{
- return $this->exporters[$name] ?? throw new \InvalidArgumentException("Exporter \"$name\" does not exist");
- }
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
- public function addExporter(string $name, string $type, array $options = []): static
- {
- $this->exporters[$name] = $this->getExporterFactory()->create($name, $type, $options);
+ if (isset($this->unresolvedExporters[$name])) {
+ return $this->resolveExporter($name);
+ }
- return $this;
+ if (isset($this->exporters[$name])) {
+ return $this->exporters[$name];
+ }
+
+ throw new InvalidArgumentException(sprintf('The exporter with the name "%s" does not exist.', $name));
}
- public function removeExporter(string $name): static
+ public function addExporter(string|ExporterBuilderInterface $exporter, string $type = null, array $options = []): static
{
- unset($this->exporters[$name]);
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
- return $this;
- }
-
- public function getColumnFactory(): ColumnFactoryInterface
- {
- return $this->columnFactory;
- }
-
- public function setColumnFactory(ColumnFactoryInterface $columnFactory): static
- {
- $this->columnFactory = $columnFactory;
-
- return $this;
- }
-
- public function getFilterFactory(): FilterFactoryInterface
- {
- return $this->filterFactory;
- }
-
- public function setFilterFactory(FilterFactoryInterface $filterFactory): static
- {
- $this->filterFactory = $filterFactory;
-
- return $this;
- }
-
- public function getActionFactory(): ActionFactoryInterface
- {
- return $this->actionFactory;
- }
-
- public function setActionFactory(ActionFactoryInterface $actionFactory): static
- {
- $this->actionFactory = $actionFactory;
-
- return $this;
- }
-
- public function getExporterFactory(): ExporterFactoryInterface
- {
- return $this->exporterFactory;
- }
-
- public function setExporterFactory(ExporterFactoryInterface $exporterFactory): static
- {
- $this->exporterFactory = $exporterFactory;
-
- return $this;
- }
-
- public function isExportingEnabled(): bool
- {
- return $this->exportingEnabled;
- }
+ if ($exporter instanceof ColumnBuilderInterface) {
+ $this->exporters[$exporter->getName()] = $exporter;
- public function setExportingEnabled(bool $exportingEnabled): static
- {
- $this->exportingEnabled = $exportingEnabled;
+ unset($this->unresolvedExporters[$exporter->getName()]);
- return $this;
- }
-
- public function getExportFormFactory(): ?FormFactoryInterface
- {
- return $this->exportFormFactory;
- }
-
- public function setExportFormFactory(?FormFactoryInterface $exportFormFactory): static
- {
- $this->exportFormFactory = $exportFormFactory;
-
- return $this;
- }
-
- public function getDefaultExportData(): ?ExportData
- {
- return $this->defaultExportData;
- }
-
- public function setDefaultExportData(?ExportData $defaultExportData): static
- {
- $this->defaultExportData = $defaultExportData;
-
- return $this;
- }
-
- public function isPersonalizationEnabled(): bool
- {
- return $this->personalizationEnabled;
- }
-
- public function setPersonalizationEnabled(bool $personalizationEnabled): static
- {
- $this->personalizationEnabled = $personalizationEnabled;
-
- return $this;
- }
-
- public function isPersonalizationPersistenceEnabled(): bool
- {
- return $this->personalizationPersistenceEnabled;
- }
-
- public function setPersonalizationPersistenceEnabled(bool $personalizationPersistenceEnabled): static
- {
- $this->personalizationPersistenceEnabled = $personalizationPersistenceEnabled;
-
- return $this;
- }
-
- public function getPersonalizationPersistenceAdapter(): ?PersistenceAdapterInterface
- {
- return $this->personalizationPersistenceAdapter;
- }
-
- public function setPersonalizationPersistenceAdapter(?PersistenceAdapterInterface $personalizationPersistenceAdapter): static
- {
- $this->personalizationPersistenceAdapter = $personalizationPersistenceAdapter;
-
- return $this;
- }
-
- public function getPersonalizationPersistenceSubject(): ?PersistenceSubjectInterface
- {
- return $this->personalizationPersistenceSubject;
- }
-
- public function setPersonalizationPersistenceSubject(?PersistenceSubjectInterface $personalizationPersistenceSubject): static
- {
- $this->personalizationPersistenceSubject = $personalizationPersistenceSubject;
-
- return $this;
- }
-
- public function getPersonalizationFormFactory(): FormFactoryInterface
- {
- return $this->personalizationFormFactory;
- }
-
- public function setPersonalizationFormFactory(?FormFactoryInterface $personalizationFormFactory): static
- {
- $this->personalizationFormFactory = $personalizationFormFactory;
-
- return $this;
- }
-
- public function getDefaultPersonalizationData(): ?PersonalizationData
- {
- return $this->defaultPersonalizationData;
- }
-
- public function setDefaultPersonalizationData(?PersonalizationData $defaultPersonalizationData): static
- {
- $this->defaultPersonalizationData = $defaultPersonalizationData;
-
- return $this;
- }
-
- public function isFiltrationEnabled(): bool
- {
- return $this->filtrationEnabled;
- }
-
- public function setFiltrationEnabled(bool $filtrationEnabled): static
- {
- $this->filtrationEnabled = $filtrationEnabled;
-
- return $this;
- }
-
- public function isFiltrationPersistenceEnabled(): bool
- {
- return $this->filtrationPersistenceEnabled;
- }
-
- public function setFiltrationPersistenceEnabled(bool $filtrationPersistenceEnabled): static
- {
- $this->filtrationPersistenceEnabled = $filtrationPersistenceEnabled;
-
- return $this;
- }
-
- public function getFiltrationPersistenceAdapter(): ?PersistenceAdapterInterface
- {
- return $this->filtrationPersistenceAdapter;
- }
-
- public function setFiltrationPersistenceAdapter(?PersistenceAdapterInterface $filtrationPersistenceAdapter): static
- {
- $this->filtrationPersistenceAdapter = $filtrationPersistenceAdapter;
-
- return $this;
- }
-
- public function getFiltrationPersistenceSubject(): ?PersistenceSubjectInterface
- {
- return $this->filtrationPersistenceSubject;
- }
-
- public function setFiltrationPersistenceSubject(?PersistenceSubjectInterface $filtrationPersistenceSubject): static
- {
- $this->filtrationPersistenceSubject = $filtrationPersistenceSubject;
-
- return $this;
- }
-
- public function getFiltrationFormFactory(): ?FormFactoryInterface
- {
- return $this->filtrationFormFactory;
- }
-
- public function setFiltrationFormFactory(?FormFactoryInterface $filtrationFormFactory): static
- {
- $this->filtrationFormFactory = $filtrationFormFactory;
-
- return $this;
- }
-
- public function getDefaultFiltrationData(): ?FiltrationData
- {
- return $this->defaultFiltrationData;
- }
-
- public function setDefaultFiltrationData(?FiltrationData $defaultFiltrationData): static
- {
- $this->defaultFiltrationData = $defaultFiltrationData;
-
- return $this;
- }
-
- public function isSortingEnabled(): bool
- {
- return $this->sortingEnabled;
- }
-
- public function setSortingEnabled(bool $sortingEnabled): static
- {
- $this->sortingEnabled = $sortingEnabled;
-
- return $this;
- }
-
- public function isSortingPersistenceEnabled(): bool
- {
- return $this->sortingPersistenceEnabled;
- }
-
- public function setSortingPersistenceEnabled(bool $sortingPersistenceEnabled): static
- {
- $this->sortingPersistenceEnabled = $sortingPersistenceEnabled;
-
- return $this;
- }
-
- public function getSortingPersistenceAdapter(): ?PersistenceAdapterInterface
- {
- return $this->sortingPersistenceAdapter;
- }
+ return $this;
+ }
- public function setSortingPersistenceAdapter(?PersistenceAdapterInterface $sortingPersistenceAdapter): static
- {
- $this->sortingPersistenceAdapter = $sortingPersistenceAdapter;
+ $this->exporters[$exporter] = null;
+ $this->unresolvedExporters[$exporter] = [$type ?? ExporterType::class, $options];
return $this;
}
- public function getSortingPersistenceSubject(): ?PersistenceSubjectInterface
- {
- return $this->sortingPersistenceSubject;
- }
-
- public function setSortingPersistenceSubject(?PersistenceSubjectInterface $sortingPersistenceSubject): static
+ public function hasExporter(string $name): bool
{
- $this->sortingPersistenceSubject = $sortingPersistenceSubject;
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
- return $this;
+ return array_key_exists($name, $this->exporters)
+ || array_key_exists($name, $this->unresolvedExporters);
}
- public function getDefaultSortingData(): ?SortingData
+ public function removeExporter(string $name): static
{
- return $this->defaultSortingData;
- }
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
- public function setDefaultSortingData(?SortingData $defaultSortingData): static
- {
- $this->defaultSortingData = $defaultSortingData;
+ unset($this->unresolvedExporters[$name], $this->exporters[$name]);
return $this;
}
- public function isPaginationEnabled(): bool
- {
- return $this->paginationEnabled;
- }
-
- public function setPaginationEnabled(bool $paginationEnabled): static
+ public function getDataTable(): DataTableInterface
{
- $this->paginationEnabled = $paginationEnabled;
-
- return $this;
- }
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
- public function isPaginationPersistenceEnabled(): bool
- {
- return $this->paginationPersistenceEnabled;
- }
+ if (null === $this->query) {
+ throw new BadMethodCallException(sprintf('Unable to create data table without a query. Use the "%s::setQuery()" method to set a query.', $this::class));
+ }
- public function setPaginationPersistenceEnabled(bool $paginationPersistenceEnabled): static
- {
- $this->paginationPersistenceEnabled = $paginationPersistenceEnabled;
+ $dataTable = new DataTable(clone $this->query, $this->getDataTableConfig());
- return $this;
- }
+ if ($this->shouldPrependBatchCheckboxColumn()) {
+ $this->prependBatchCheckboxColumn();
+ }
- public function getPaginationPersistenceAdapter(): ?PersistenceAdapterInterface
- {
- return $this->paginationPersistenceAdapter;
- }
+ if ($this->shouldAppendActionsColumn()) {
+ $this->appendActionsColumn();
+ }
- public function setPaginationPersistenceAdapter(?PersistenceAdapterInterface $paginationPersistenceAdapter): static
- {
- $this->paginationPersistenceAdapter = $paginationPersistenceAdapter;
+ if ($this->shouldAddSearchFilter()) {
+ $this->addSearchFilter();
+ }
- return $this;
- }
+ $this->resolveColumns();
- public function getPaginationPersistenceSubject(): ?PersistenceSubjectInterface
- {
- return $this->paginationPersistenceSubject;
- }
+ foreach ($this->columns as $column) {
+ $dataTable->addColumn($column->getColumn());
+ }
- public function setPaginationPersistenceSubject(?PersistenceSubjectInterface $paginationPersistenceSubject): static
- {
- $this->paginationPersistenceSubject = $paginationPersistenceSubject;
+ $this->resolveFilters();
- return $this;
- }
+ foreach ($this->filters as $filter) {
+ $dataTable->addFilter($filter->getFilter());
+ }
- public function getDefaultPaginationData(): ?PaginationData
- {
- return $this->defaultPaginationData;
- }
+ $this->resolveActions();
- public function setDefaultPaginationData(?PaginationData $defaultPaginationData): static
- {
- $this->defaultPaginationData = $defaultPaginationData;
+ foreach ($this->actions as $action) {
+ $dataTable->addAction($action->getAction());
+ }
- return $this;
- }
+ $this->resolveBatchActions();
- public function getRequestHandler(): ?RequestHandlerInterface
- {
- return $this->requestHandler;
- }
+ foreach ($this->batchActions as $batchAction) {
+ $dataTable->addBatchAction($batchAction->getAction());
+ }
- public function setRequestHandler(?RequestHandlerInterface $requestHandler): static
- {
- $this->requestHandler = $requestHandler;
+ $this->resolveRowActions();
- return $this;
- }
+ foreach ($this->rowActions as $rowAction) {
+ $dataTable->addRowAction($rowAction->getAction());
+ }
- public function getHeaderRowAttributes(): array
- {
- return $this->headerRowAttributes;
- }
+ $this->resolveExporters();
- public function setHeaderRowAttributes(array $headerRowAttributes): static
- {
- $this->headerRowAttributes = $headerRowAttributes;
+ foreach ($this->exporters as $exporter) {
+ $dataTable->addExporter($exporter->getExporter());
+ }
- return $this;
- }
+ // TODO: Remove initialization logic from builder.
+ // Instead, add "initialized" flag to the data table itself to allow lazy initialization.
+ $dataTable->initialize();
- public function getValueRowAttributes(): array
- {
- return $this->valueRowAttributes;
+ return $dataTable;
}
- public function setValueRowAttributes(array $valueRowAttributes): static
+ private function resolveColumn(string $name): ColumnBuilderInterface
{
- $this->valueRowAttributes = $valueRowAttributes;
+ [$type, $options] = $this->unresolvedColumns[$name];
- return $this;
- }
+ unset($this->unresolvedColumns[$name]);
- public function getPageParameterName(): string
- {
- return $this->getParameterName(static::PAGE_PARAMETER);
+ return $this->columns[$name] = $this->getColumnFactory()->createNamedBuilder($name, $type, $options);
}
- public function getPerPageParameterName(): string
+ private function resolveColumns(): void
{
- return $this->getParameterName(static::PER_PAGE_PARAMETER);
+ foreach (array_keys($this->unresolvedColumns) as $column) {
+ $this->resolveColumn($column);
+ }
}
- public function getSortParameterName(): string
+ private function resolveFilter(string $name): FilterBuilderInterface
{
- return $this->getParameterName(static::SORT_PARAMETER);
- }
+ [$type, $options] = $this->unresolvedFilters[$name];
- public function getFiltrationParameterName(): string
- {
- return $this->getParameterName(static::FILTRATION_PARAMETER);
- }
+ unset($this->unresolvedFilters[$name]);
- public function getPersonalizationParameterName(): string
- {
- return $this->getParameterName(static::PERSONALIZATION_PARAMETER);
+ return $this->filters[$name] = $this->getFilterFactory()->createNamedBuilder($name, $type, $options);
}
- public function getExportParameterName(): string
- {
- return $this->getParameterName(static::EXPORT_PARAMETER);
- }
-
- public function getDataTable(): DataTableInterface
+ private function resolveFilters(): void
{
- $this->validate();
-
- if ($this->shouldPrependBatchCheckboxColumn()) {
- $this->prependBatchCheckboxColumn();
- }
-
- $this->resolveRowActions();
-
- if ($this->shouldAppendActionsColumn()) {
- $this->appendActionsColumn();
- }
-
- $dataTable = new DataTable(
- query: clone $this->query,
- config: $this->getDataTableConfig(),
- );
-
- $this->resolveActions();
-
- foreach ($this->actions as $action) {
- $dataTable->addAction($action->getAction());
- }
-
- $this->resolveBatchActions();
-
- foreach ($this->batchActions as $batchAction) {
- $dataTable->addBatchAction($batchAction->getAction());
- }
-
- foreach ($this->rowActions as $rowAction) {
- $dataTable->addRowAction($rowAction->getAction());
+ foreach (array_keys($this->unresolvedFilters) as $filter) {
+ $this->resolveFilter($filter);
}
-
- $dataTable->initialize();
-
- return $dataTable;
- }
-
- public function getDataTableConfig(): DataTableConfigInterface
- {
- $config = clone $this;
- $config->locked = true;
-
- return $config;
}
private function resolveAction(string $name): ActionBuilderInterface
@@ -1205,34 +821,20 @@ private function resolveRowActions(): void
}
}
- private function validate(): void
+ private function resolveExporter(string $name): ExporterBuilderInterface
{
- if (null === $this->query) {
- throw new \LogicException('The data table has no proxy query. You must provide it using either the data table factory or the builder "setQuery()" method.');
- }
-
- if (empty($this->columns)) {
- throw new \LogicException('The data table has no configured columns. You must provide them using the builder "addColumn()" method.');
- }
-
- foreach (static::PERSISTENCE_CONTEXTS as $context) {
- if (!$this->{$context.'Enabled'} || !$this->{$context.'PersistenceEnabled'}) {
- continue;
- }
+ [$type, $options] = $this->unresolvedExporters[$name];
- if (null === $this->{$context.'PersistenceAdapter'}) {
- throw new \LogicException("The data table is configured to use $context persistence, but does not have an adapter.");
- }
+ unset($this->unresolvedExporters[$name]);
- if (null === $this->{$context.'PersistenceSubject'}) {
- throw new \LogicException("The data table is configured to use $context persistence, but does not have a subject.");
- }
- }
+ return $this->exporters[$name] = $this->getExporterFactory()->createNamedBuilder($name, $type, $options);
}
- private function getParameterName(string $prefix): string
+ private function resolveExporters(): void
{
- return implode('_', array_filter([$prefix, $this->name]));
+ foreach (array_keys($this->unresolvedExporters) as $exporter) {
+ $this->resolveExporter($exporter);
+ }
}
private function shouldPrependBatchCheckboxColumn(): bool
@@ -1249,20 +851,37 @@ private function shouldAppendActionsColumn(): bool
&& !$this->hasColumn(self::ACTIONS_COLUMN_NAME);
}
- private function prependBatchCheckboxColumn(): void
+ private function shouldAddSearchFilter(): bool
{
- $this->addColumn(self::BATCH_CHECKBOX_COLUMN_NAME, CheckboxColumnType::class);
+ return $this->isAutoAddingSearchFilter()
+ && null !== $this->getSearchHandler()
+ && !$this->hasFilter(self::SEARCH_FILTER_NAME);
+ }
- $this->columns = [
- self::BATCH_CHECKBOX_COLUMN_NAME => $this->getColumn(self::BATCH_CHECKBOX_COLUMN_NAME),
- ...$this->getColumns(),
- ];
+ private function prependBatchCheckboxColumn(): void
+ {
+ $this->addColumn(self::BATCH_CHECKBOX_COLUMN_NAME, CheckboxColumnType::class, [
+ 'priority' => self::BATCH_CHECKBOX_COLUMN_PRIORITY,
+ ]);
}
private function appendActionsColumn(): void
{
$this->addColumn(self::ACTIONS_COLUMN_NAME, ActionsColumnType::class, [
- 'actions' => $this->rowActions,
+ 'priority' => self::ACTIONS_COLUMN_PRIORITY,
+ 'actions' => $this->getRowActions(),
]);
}
+
+ private function addSearchFilter(): void
+ {
+ $this->addFilter(self::SEARCH_FILTER_NAME, SearchFilterType::class, [
+ 'handler' => $this->getSearchHandler(),
+ ]);
+ }
+
+ private function createBuilderLockedException(): BadMethodCallException
+ {
+ return new BadMethodCallException('DataTableBuilder methods cannot be accessed anymore once the builder is turned into a DataTableConfigInterface instance.');
+ }
}
diff --git a/src/DataTableBuilderInterface.php b/src/DataTableBuilderInterface.php
old mode 100644
new mode 100755
index ea6a9a27..1015dbe4
--- a/src/DataTableBuilderInterface.php
+++ b/src/DataTableBuilderInterface.php
@@ -6,15 +6,73 @@
use Kreyu\Bundle\DataTableBundle\Action\ActionBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Action\Type\ActionTypeInterface;
+use Kreyu\Bundle\DataTableBundle\Column\ColumnBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Column\Type\ColumnTypeInterface;
use Kreyu\Bundle\DataTableBundle\Exception\InvalidArgumentException;
+use Kreyu\Bundle\DataTableBundle\Exporter\ExporterBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Exporter\Type\ExporterTypeInterface;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Filter\Type\FilterTypeInterface;
use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
interface DataTableBuilderInterface extends DataTableConfigBuilderInterface
{
public const BATCH_CHECKBOX_COLUMN_NAME = '__batch';
+ public const BATCH_CHECKBOX_COLUMN_PRIORITY = 1000;
+
public const ACTIONS_COLUMN_NAME = '__actions';
+ public const ACTIONS_COLUMN_PRIORITY = -1;
+
+ public const SEARCH_FILTER_NAME = '__search';
+
+ /**
+ * @return array
+ */
+ public function getColumns(): array;
+
+ /**
+ * @throws InvalidArgumentException if column of given name does not exist
+ */
+ public function getColumn(string $name): ColumnBuilderInterface;
+
+ public function hasColumn(string $name): bool;
+
+ /**
+ * @param class-string|null $type
+ */
+ public function addColumn(ColumnBuilderInterface|string $column, string $type = null, array $options = []): static;
+
+ public function removeColumn(string $name): static;
+
+ /**
+ * @return array
+ */
+ public function getFilters(): array;
+
+ /**
+ * @throws InvalidArgumentException if filter of given name does not exist
+ */
+ public function getFilter(string $name): FilterBuilderInterface;
+
+ public function hasFilter(string $name): bool;
+
+ /**
+ * @param class-string|null $type
+ */
+ public function addFilter(FilterBuilderInterface|string $filter, string $type = null, array $options = []): static;
+
+ public function removeFilter(string $name): static;
+
+ public function getSearchHandler(): ?callable;
+
+ public function setSearchHandler(?callable $searchHandler): static;
+
+ public function isAutoAddingSearchFilter(): bool;
+
+ public function setAutoAddingSearchFilter(bool $autoAddingSearchFilter): static;
+
/**
* @return array
*/
@@ -80,6 +138,25 @@ public function isAutoAddingActionsColumn(): bool;
public function setAutoAddingActionsColumn(bool $autoAddingActionsColumn): static;
+ /**
+ * @return array
+ */
+ public function getExporters(): array;
+
+ /**
+ * @throws InvalidArgumentException if exporter of given name does not exist
+ */
+ public function getExporter(string $name): ExporterBuilderInterface;
+
+ public function hasExporter(string $name): bool;
+
+ /**
+ * @param class-string|null $type
+ */
+ public function addExporter(ExporterBuilderInterface|string $exporter, string $type = null, array $options = []): static;
+
+ public function removeExporter(string $name): static;
+
public function getQuery(): ?ProxyQueryInterface;
public function setQuery(?ProxyQueryInterface $query): static;
diff --git a/src/DataTableConfigBuilder.php b/src/DataTableConfigBuilder.php
new file mode 100755
index 00000000..1a574b56
--- /dev/null
+++ b/src/DataTableConfigBuilder.php
@@ -0,0 +1,819 @@
+dispatcher->addListener($eventName, $listener, $priority);
+
+ return $this;
+ }
+
+ public function addEventSubscriber(EventSubscriberInterface $subscriber): static
+ {
+ $this->dispatcher->addSubscriber($subscriber);
+
+ return $this;
+ }
+
+ public function getEventDispatcher(): EventDispatcherInterface
+ {
+ if (!$this->dispatcher instanceof ImmutableEventDispatcher) {
+ $this->dispatcher = new ImmutableEventDispatcher($this->dispatcher);
+ }
+
+ return $this->dispatcher;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function setName(string $name): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->name = $name;
+
+ return $this;
+ }
+
+ public function getType(): ResolvedDataTableTypeInterface
+ {
+ return $this->type;
+ }
+
+ public function setType(ResolvedDataTableTypeInterface $type): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->type = $type;
+
+ return $this;
+ }
+
+ public function getOptions(): array
+ {
+ return $this->options;
+ }
+
+ public function hasOption(string $name): bool
+ {
+ return array_key_exists($name, $this->options);
+ }
+
+ public function getOption(string $name, mixed $default = null): mixed
+ {
+ return $this->options[$name] ?? $default;
+ }
+
+ public function setOptions(array $options): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->options = $options;
+
+ return $this;
+ }
+
+ public function setOption(string $name, mixed $value): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->options[$name] = $value;
+
+ return $this;
+ }
+
+ public function getColumnFactory(): ColumnFactoryInterface
+ {
+ if (!isset($this->columnFactory)) {
+ throw new BadMethodCallException(sprintf('The column factory is not set, use the "%s::setColumnFactory()" method to set the column factory.', $this::class));
+ }
+
+ return $this->columnFactory;
+ }
+
+ public function setColumnFactory(ColumnFactoryInterface $columnFactory): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->columnFactory = $columnFactory;
+
+ return $this;
+ }
+
+ public function getFilterFactory(): FilterFactoryInterface
+ {
+ if (!isset($this->filterFactory)) {
+ throw new BadMethodCallException(sprintf('The filter factory is not set, use the "%s::setFilterFactory()" method to set the filter factory.', $this::class));
+ }
+
+ return $this->filterFactory;
+ }
+
+ public function setFilterFactory(FilterFactoryInterface $filterFactory): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->filterFactory = $filterFactory;
+
+ return $this;
+ }
+
+ public function getActionFactory(): ActionFactoryInterface
+ {
+ if (!isset($this->actionFactory)) {
+ throw new BadMethodCallException(sprintf('The action factory is not set, use the "%s::setActionFactory()" method to set the action factory.', $this::class));
+ }
+
+ return $this->actionFactory;
+ }
+
+ public function setActionFactory(ActionFactoryInterface $actionFactory): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->actionFactory = $actionFactory;
+
+ return $this;
+ }
+
+ public function getExporterFactory(): ExporterFactoryInterface
+ {
+ if (!isset($this->exporterFactory)) {
+ throw new BadMethodCallException(sprintf('The exporter factory is not set, use the "%s::setExporterFactory()" method to set the exporter factory.', $this::class));
+ }
+
+ return $this->exporterFactory;
+ }
+
+ public function setExporterFactory(ExporterFactoryInterface $exporterFactory): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->exporterFactory = $exporterFactory;
+
+ return $this;
+ }
+
+ public function isExportingEnabled(): bool
+ {
+ return $this->exportingEnabled;
+ }
+
+ public function setExportingEnabled(bool $exportingEnabled): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->exportingEnabled = $exportingEnabled;
+
+ return $this;
+ }
+
+ public function getExportFormFactory(): ?FormFactoryInterface
+ {
+ return $this->exportFormFactory;
+ }
+
+ public function setExportFormFactory(?FormFactoryInterface $exportFormFactory): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->exportFormFactory = $exportFormFactory;
+
+ return $this;
+ }
+
+ public function getDefaultExportData(): ?ExportData
+ {
+ return $this->defaultExportData;
+ }
+
+ public function setDefaultExportData(?ExportData $defaultExportData): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->defaultExportData = $defaultExportData;
+
+ return $this;
+ }
+
+ public function isPersonalizationEnabled(): bool
+ {
+ return $this->personalizationEnabled;
+ }
+
+ public function setPersonalizationEnabled(bool $personalizationEnabled): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->personalizationEnabled = $personalizationEnabled;
+
+ return $this;
+ }
+
+ public function isPersonalizationPersistenceEnabled(): bool
+ {
+ return $this->personalizationPersistenceEnabled;
+ }
+
+ public function setPersonalizationPersistenceEnabled(bool $personalizationPersistenceEnabled): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->personalizationPersistenceEnabled = $personalizationPersistenceEnabled;
+
+ return $this;
+ }
+
+ public function getPersonalizationPersistenceAdapter(): ?PersistenceAdapterInterface
+ {
+ return $this->personalizationPersistenceAdapter;
+ }
+
+ public function setPersonalizationPersistenceAdapter(?PersistenceAdapterInterface $personalizationPersistenceAdapter): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->personalizationPersistenceAdapter = $personalizationPersistenceAdapter;
+
+ return $this;
+ }
+
+ public function getPersonalizationPersistenceSubjectProvider(): ?PersistenceSubjectProviderInterface
+ {
+ return $this->personalizationPersistenceSubjectProvider;
+ }
+
+ public function setPersonalizationPersistenceSubjectProvider(?PersistenceSubjectProviderInterface $personalizationPersistenceSubjectProvider): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->personalizationPersistenceSubjectProvider = $personalizationPersistenceSubjectProvider;
+
+ return $this;
+ }
+
+ public function getPersonalizationFormFactory(): FormFactoryInterface
+ {
+ return $this->personalizationFormFactory;
+ }
+
+ public function setPersonalizationFormFactory(?FormFactoryInterface $personalizationFormFactory): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->personalizationFormFactory = $personalizationFormFactory;
+
+ return $this;
+ }
+
+ public function getDefaultPersonalizationData(): ?PersonalizationData
+ {
+ return $this->defaultPersonalizationData;
+ }
+
+ public function setDefaultPersonalizationData(?PersonalizationData $defaultPersonalizationData): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->defaultPersonalizationData = $defaultPersonalizationData;
+
+ return $this;
+ }
+
+ public function isFiltrationEnabled(): bool
+ {
+ return $this->filtrationEnabled;
+ }
+
+ public function setFiltrationEnabled(bool $filtrationEnabled): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->filtrationEnabled = $filtrationEnabled;
+
+ return $this;
+ }
+
+ public function isFiltrationPersistenceEnabled(): bool
+ {
+ return $this->filtrationPersistenceEnabled;
+ }
+
+ public function setFiltrationPersistenceEnabled(bool $filtrationPersistenceEnabled): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->filtrationPersistenceEnabled = $filtrationPersistenceEnabled;
+
+ return $this;
+ }
+
+ public function getFiltrationPersistenceAdapter(): ?PersistenceAdapterInterface
+ {
+ return $this->filtrationPersistenceAdapter;
+ }
+
+ public function setFiltrationPersistenceAdapter(?PersistenceAdapterInterface $filtrationPersistenceAdapter): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->filtrationPersistenceAdapter = $filtrationPersistenceAdapter;
+
+ return $this;
+ }
+
+ public function getFiltrationPersistenceSubjectProvider(): ?PersistenceSubjectProviderInterface
+ {
+ return $this->filtrationPersistenceSubjectProvider;
+ }
+
+ public function setFiltrationPersistenceSubjectProvider(?PersistenceSubjectProviderInterface $filtrationPersistenceSubjectProvider): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->filtrationPersistenceSubjectProvider = $filtrationPersistenceSubjectProvider;
+
+ return $this;
+ }
+
+ public function getFiltrationFormFactory(): ?FormFactoryInterface
+ {
+ return $this->filtrationFormFactory;
+ }
+
+ public function setFiltrationFormFactory(?FormFactoryInterface $filtrationFormFactory): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->filtrationFormFactory = $filtrationFormFactory;
+
+ return $this;
+ }
+
+ public function getDefaultFiltrationData(): ?FiltrationData
+ {
+ return $this->defaultFiltrationData;
+ }
+
+ public function setDefaultFiltrationData(?FiltrationData $defaultFiltrationData): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->defaultFiltrationData = $defaultFiltrationData;
+
+ return $this;
+ }
+
+ public function isSortingEnabled(): bool
+ {
+ return $this->sortingEnabled;
+ }
+
+ public function setSortingEnabled(bool $sortingEnabled): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->sortingEnabled = $sortingEnabled;
+
+ return $this;
+ }
+
+ public function isSortingPersistenceEnabled(): bool
+ {
+ return $this->sortingPersistenceEnabled;
+ }
+
+ public function setSortingPersistenceEnabled(bool $sortingPersistenceEnabled): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->sortingPersistenceEnabled = $sortingPersistenceEnabled;
+
+ return $this;
+ }
+
+ public function getSortingPersistenceAdapter(): ?PersistenceAdapterInterface
+ {
+ return $this->sortingPersistenceAdapter;
+ }
+
+ public function setSortingPersistenceAdapter(?PersistenceAdapterInterface $sortingPersistenceAdapter): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->sortingPersistenceAdapter = $sortingPersistenceAdapter;
+
+ return $this;
+ }
+
+ public function getSortingPersistenceSubjectProvider(): ?PersistenceSubjectProviderInterface
+ {
+ return $this->sortingPersistenceSubjectProvider;
+ }
+
+ public function setSortingPersistenceSubjectProvider(?PersistenceSubjectProviderInterface $sortingPersistenceSubjectProvider): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->sortingPersistenceSubjectProvider = $sortingPersistenceSubjectProvider;
+
+ return $this;
+ }
+
+ public function getDefaultSortingData(): ?SortingData
+ {
+ return $this->defaultSortingData;
+ }
+
+ public function setDefaultSortingData(?SortingData $defaultSortingData): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->defaultSortingData = $defaultSortingData;
+
+ return $this;
+ }
+
+ public function isPaginationEnabled(): bool
+ {
+ return $this->paginationEnabled;
+ }
+
+ public function setPaginationEnabled(bool $paginationEnabled): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->paginationEnabled = $paginationEnabled;
+
+ return $this;
+ }
+
+ public function isPaginationPersistenceEnabled(): bool
+ {
+ return $this->paginationPersistenceEnabled;
+ }
+
+ public function setPaginationPersistenceEnabled(bool $paginationPersistenceEnabled): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->paginationPersistenceEnabled = $paginationPersistenceEnabled;
+
+ return $this;
+ }
+
+ public function getPaginationPersistenceAdapter(): ?PersistenceAdapterInterface
+ {
+ return $this->paginationPersistenceAdapter;
+ }
+
+ public function setPaginationPersistenceAdapter(?PersistenceAdapterInterface $paginationPersistenceAdapter): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->paginationPersistenceAdapter = $paginationPersistenceAdapter;
+
+ return $this;
+ }
+
+ public function getPaginationPersistenceSubjectProvider(): ?PersistenceSubjectProviderInterface
+ {
+ return $this->paginationPersistenceSubjectProvider;
+ }
+
+ public function setPaginationPersistenceSubjectProvider(?PersistenceSubjectProviderInterface $paginationPersistenceSubjectProvider): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->paginationPersistenceSubjectProvider = $paginationPersistenceSubjectProvider;
+
+ return $this;
+ }
+
+ public function getDefaultPaginationData(): ?PaginationData
+ {
+ return $this->defaultPaginationData;
+ }
+
+ public function setDefaultPaginationData(?PaginationData $defaultPaginationData): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->defaultPaginationData = $defaultPaginationData;
+
+ return $this;
+ }
+
+ public function getRequestHandler(): ?RequestHandlerInterface
+ {
+ return $this->requestHandler;
+ }
+
+ public function setRequestHandler(?RequestHandlerInterface $requestHandler): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->requestHandler = $requestHandler;
+
+ return $this;
+ }
+
+ public function getThemes(): array
+ {
+ return $this->themes;
+ }
+
+ public function addTheme(string $theme): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->themes[] = $theme;
+
+ return $this;
+ }
+
+ public function setThemes(array $themes): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->themes = $themes;
+
+ return $this;
+ }
+
+ public function getHeaderRowAttributes(): array
+ {
+ return $this->headerRowAttributes;
+ }
+
+ public function hasHeaderRowAttribute(string $name): bool
+ {
+ return array_key_exists($name, $this->headerRowAttributes);
+ }
+
+ public function getHeaderRowAttribute(string $name, mixed $default = null): mixed
+ {
+ return $this->headerRowAttributes[$name] ?? $default;
+ }
+
+ public function setHeaderRowAttribute(string $name, mixed $value): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->headerRowAttributes[$name] = $value;
+
+ return $this;
+ }
+
+ public function setHeaderRowAttributes(array $headerRowAttributes): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->headerRowAttributes = $headerRowAttributes;
+
+ return $this;
+ }
+
+ public function getValueRowAttributes(): array
+ {
+ return $this->valueRowAttributes;
+ }
+
+ public function hasValueRowAttribute(string $name): bool
+ {
+ return array_key_exists($name, $this->valueRowAttributes);
+ }
+
+ public function getValueRowAttribute(string $name, mixed $default = null): mixed
+ {
+ return $this->valueRowAttributes[$name] ?? $default;
+ }
+
+ public function setValueRowAttribute(string $name, mixed $value): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->valueRowAttributes[$name] = $value;
+
+ return $this;
+ }
+
+ public function setValueRowAttributes(array $valueRowAttributes): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->valueRowAttributes = $valueRowAttributes;
+
+ return $this;
+ }
+
+ public function getPageParameterName(): string
+ {
+ return $this->getParameterName(static::PAGE_PARAMETER);
+ }
+
+ public function getPerPageParameterName(): string
+ {
+ return $this->getParameterName(static::PER_PAGE_PARAMETER);
+ }
+
+ public function getSortParameterName(): string
+ {
+ return $this->getParameterName(static::SORT_PARAMETER);
+ }
+
+ public function getFiltrationParameterName(): string
+ {
+ return $this->getParameterName(static::FILTRATION_PARAMETER);
+ }
+
+ public function getPersonalizationParameterName(): string
+ {
+ return $this->getParameterName(static::PERSONALIZATION_PARAMETER);
+ }
+
+ public function getExportParameterName(): string
+ {
+ return $this->getParameterName(static::EXPORT_PARAMETER);
+ }
+
+ public function getDataTableConfig(): DataTableConfigInterface
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $config = clone $this;
+ $config->locked = true;
+
+ return $config;
+ }
+
+ private function getParameterName(string $prefix): string
+ {
+ return implode('_', array_filter([$prefix, $this->name]));
+ }
+
+ private function createBuilderLockedException(): BadMethodCallException
+ {
+ return new BadMethodCallException('DataTableConfigBuilder methods cannot be accessed anymore once the builder is turned into a DataTableConfigInterface instance.');
+ }
+}
diff --git a/src/DataTableConfigBuilderInterface.php b/src/DataTableConfigBuilderInterface.php
old mode 100644
new mode 100755
index 9b70af3c..0cf609ee
--- a/src/DataTableConfigBuilderInterface.php
+++ b/src/DataTableConfigBuilderInterface.php
@@ -6,78 +6,49 @@
use Kreyu\Bundle\DataTableBundle\Action\ActionFactoryInterface;
use Kreyu\Bundle\DataTableBundle\Column\ColumnFactoryInterface;
-use Kreyu\Bundle\DataTableBundle\Column\Type\ColumnTypeInterface;
use Kreyu\Bundle\DataTableBundle\Exporter\ExportData;
use Kreyu\Bundle\DataTableBundle\Exporter\ExporterFactoryInterface;
-use Kreyu\Bundle\DataTableBundle\Exporter\Type\ExporterTypeInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FilterFactoryInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FiltrationData;
-use Kreyu\Bundle\DataTableBundle\Filter\Type\FilterTypeInterface;
use Kreyu\Bundle\DataTableBundle\Pagination\PaginationData;
use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface;
-use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectInterface;
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface;
use Kreyu\Bundle\DataTableBundle\Personalization\PersonalizationData;
use Kreyu\Bundle\DataTableBundle\Request\RequestHandlerInterface;
use Kreyu\Bundle\DataTableBundle\Sorting\SortingData;
use Kreyu\Bundle\DataTableBundle\Type\ResolvedDataTableTypeInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormFactoryInterface;
-use Symfony\Component\Translation\TranslatableMessage;
interface DataTableConfigBuilderInterface extends DataTableConfigInterface
{
- public function setName(string $name): static;
-
- public function setType(ResolvedDataTableTypeInterface $type): static;
-
- public function setOptions(array $options): static;
-
- public function setThemes(array $themes): static;
-
- public function addTheme(string $theme): static;
+ public function addEventListener(string $eventName, callable $listener, int $priority = 0): static;
- public function removeTheme(string $theme): static;
-
- public function setTitle(null|string|TranslatableMessage $title): static;
-
- public function setTitleTranslationParameters(array $titleTranslationParameters): static;
-
- public function setTranslationDomain(null|bool|string $translationDomain): static;
+ public function addEventSubscriber(EventSubscriberInterface $subscriber): static;
/**
- * @param class-string $type
+ * @deprecated since 0.14.0, provide the name using the factory {@see DataTableFactoryInterface} "named" methods instead
*/
- public function addColumn(string $name, string $type, array $options = []): static;
-
- public function removeColumn(string $name): static;
-
- public function getColumnFactory(): ColumnFactoryInterface;
+ public function setName(string $name): static;
- public function setColumnFactory(ColumnFactoryInterface $columnFactory): static;
+ public function setType(ResolvedDataTableTypeInterface $type): static;
/**
- * @param class-string $type
+ * @deprecated since 0.14.0, modifying the options dynamically will be removed as it creates unexpected behaviors
*/
- public function addFilter(string $name, string $type, array $options = []): static;
+ public function setOptions(array $options): static;
- public function removeFilter(string $name): static;
+ /**
+ * @deprecated since 0.14.0, modifying the options dynamically will be removed as it creates unexpected behaviors
+ */
+ public function setOption(string $name, mixed $value): static;
- public function getFilterFactory(): FilterFactoryInterface;
+ public function setColumnFactory(ColumnFactoryInterface $columnFactory): static;
public function setFilterFactory(FilterFactoryInterface $filterFactory): static;
- public function getActionFactory(): ActionFactoryInterface;
-
public function setActionFactory(ActionFactoryInterface $actionFactory): static;
- /**
- * @param class-string $type
- */
- public function addExporter(string $name, string $type, array $options = []): static;
-
- public function removeExporter(string $name): static;
-
- public function getExporterFactory(): ExporterFactoryInterface;
-
public function setExporterFactory(ExporterFactoryInterface $exporterFactory): static;
public function setExportingEnabled(bool $exportingEnabled): static;
@@ -92,7 +63,7 @@ public function setPersonalizationPersistenceEnabled(bool $personalizationPersis
public function setPersonalizationPersistenceAdapter(?PersistenceAdapterInterface $personalizationPersistenceAdapter): static;
- public function setPersonalizationPersistenceSubject(?PersistenceSubjectInterface $personalizationPersistenceSubject): static;
+ public function setPersonalizationPersistenceSubjectProvider(?PersistenceSubjectProviderInterface $personalizationPersistenceSubjectProvider): static;
public function setPersonalizationFormFactory(?FormFactoryInterface $personalizationFormFactory): static;
@@ -104,7 +75,7 @@ public function setFiltrationPersistenceEnabled(bool $filtrationPersistenceEnabl
public function setFiltrationPersistenceAdapter(?PersistenceAdapterInterface $filtrationPersistenceAdapter): static;
- public function setFiltrationPersistenceSubject(?PersistenceSubjectInterface $filtrationPersistenceSubject): static;
+ public function setFiltrationPersistenceSubjectProvider(?PersistenceSubjectProviderInterface $filtrationPersistenceSubjectProvider): static;
public function setFiltrationFormFactory(?FormFactoryInterface $filtrationFormFactory): static;
@@ -116,7 +87,7 @@ public function setSortingPersistenceEnabled(bool $sortingPersistenceEnabled): s
public function setSortingPersistenceAdapter(?PersistenceAdapterInterface $sortingPersistenceAdapter): static;
- public function setSortingPersistenceSubject(?PersistenceSubjectInterface $sortingPersistenceSubject): static;
+ public function setSortingPersistenceSubjectProvider(?PersistenceSubjectProviderInterface $sortingPersistenceSubjectProvider): static;
public function setDefaultSortingData(?SortingData $defaultSortingData): static;
@@ -126,14 +97,22 @@ public function setPaginationPersistenceEnabled(bool $paginationPersistenceEnabl
public function setPaginationPersistenceAdapter(?PersistenceAdapterInterface $paginationPersistenceAdapter): static;
- public function setPaginationPersistenceSubject(?PersistenceSubjectInterface $paginationPersistenceSubject): static;
+ public function setPaginationPersistenceSubjectProvider(?PersistenceSubjectProviderInterface $paginationPersistenceSubjectProvider): static;
public function setDefaultPaginationData(?PaginationData $defaultPaginationData): static;
public function setRequestHandler(?RequestHandlerInterface $requestHandler): static;
+ public function addTheme(string $theme): static;
+
+ public function setThemes(array $themes): static;
+
+ public function setHeaderRowAttribute(string $name, mixed $value): static;
+
public function setHeaderRowAttributes(array $headerRowAttributes): static;
+ public function setValueRowAttribute(string $name, mixed $value): static;
+
public function setValueRowAttributes(array $valueRowAttributes): static;
public function getDataTableConfig(): DataTableConfigInterface;
diff --git a/src/DataTableConfigInterface.php b/src/DataTableConfigInterface.php
old mode 100644
new mode 100755
index 3a8e703e..76b79703
--- a/src/DataTableConfigInterface.php
+++ b/src/DataTableConfigInterface.php
@@ -4,20 +4,21 @@
namespace Kreyu\Bundle\DataTableBundle;
-use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface;
+use Kreyu\Bundle\DataTableBundle\Action\ActionFactoryInterface;
+use Kreyu\Bundle\DataTableBundle\Column\ColumnFactoryInterface;
use Kreyu\Bundle\DataTableBundle\Exporter\ExportData;
-use Kreyu\Bundle\DataTableBundle\Exporter\ExporterInterface;
-use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
+use Kreyu\Bundle\DataTableBundle\Exporter\ExporterFactoryInterface;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterFactoryInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FiltrationData;
use Kreyu\Bundle\DataTableBundle\Pagination\PaginationData;
use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface;
-use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectInterface;
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface;
use Kreyu\Bundle\DataTableBundle\Personalization\PersonalizationData;
use Kreyu\Bundle\DataTableBundle\Request\RequestHandlerInterface;
use Kreyu\Bundle\DataTableBundle\Sorting\SortingData;
use Kreyu\Bundle\DataTableBundle\Type\ResolvedDataTableTypeInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormFactoryInterface;
-use Symfony\Component\Translation\TranslatableMessage;
interface DataTableConfigInterface
{
@@ -28,12 +29,7 @@ interface DataTableConfigInterface
public const PERSONALIZATION_PARAMETER = 'personalization';
public const EXPORT_PARAMETER = 'export';
- public const PERSISTENCE_CONTEXTS = [
- 'sorting',
- 'pagination',
- 'filtration',
- 'personalization',
- ];
+ public function getEventDispatcher(): EventDispatcherInterface;
public function getName(): string;
@@ -41,45 +37,19 @@ public function getType(): ResolvedDataTableTypeInterface;
public function getOptions(): array;
- public function getThemes(): array;
-
- public function getTitle(): null|string|TranslatableMessage;
-
- public function getTitleTranslationParameters(): array;
-
- public function getTranslationDomain(): null|bool|string;
-
- /**
- * @return array
- */
- public function getColumns(): array;
+ public function hasOption(string $name): bool;
- /**
- * @throws \InvalidArgumentException if column of given name does not exist
- */
- public function getColumn(string $name): ColumnInterface;
+ public function getOption(string $name, mixed $default = null): mixed;
- public function hasColumn(string $name): bool;
+ public function getThemes(): array;
- /**
- * @return array
- */
- public function getFilters(): array;
+ public function getColumnFactory(): ColumnFactoryInterface;
- /**
- * @throws \InvalidArgumentException if filter of given name does not exist
- */
- public function getFilter(string $name): FilterInterface;
+ public function getFilterFactory(): FilterFactoryInterface;
- /**
- * @return array
- */
- public function getExporters(): array;
+ public function getActionFactory(): ActionFactoryInterface;
- /**
- * @throws \InvalidArgumentException if exporter of given name does not exist
- */
- public function getExporter(string $name): ExporterInterface;
+ public function getExporterFactory(): ExporterFactoryInterface;
public function isExportingEnabled(): bool;
@@ -93,7 +63,7 @@ public function isPersonalizationPersistenceEnabled(): bool;
public function getPersonalizationPersistenceAdapter(): ?PersistenceAdapterInterface;
- public function getPersonalizationPersistenceSubject(): ?PersistenceSubjectInterface;
+ public function getPersonalizationPersistenceSubjectProvider(): ?PersistenceSubjectProviderInterface;
public function getPersonalizationFormFactory(): ?FormFactoryInterface;
@@ -105,7 +75,7 @@ public function isFiltrationPersistenceEnabled(): bool;
public function getFiltrationPersistenceAdapter(): ?PersistenceAdapterInterface;
- public function getFiltrationPersistenceSubject(): ?PersistenceSubjectInterface;
+ public function getFiltrationPersistenceSubjectProvider(): ?PersistenceSubjectProviderInterface;
public function getFiltrationFormFactory(): ?FormFactoryInterface;
@@ -117,7 +87,7 @@ public function isSortingPersistenceEnabled(): bool;
public function getSortingPersistenceAdapter(): ?PersistenceAdapterInterface;
- public function getSortingPersistenceSubject(): ?PersistenceSubjectInterface;
+ public function getSortingPersistenceSubjectProvider(): ?PersistenceSubjectProviderInterface;
public function getDefaultSortingData(): ?SortingData;
@@ -127,7 +97,7 @@ public function isPaginationPersistenceEnabled(): bool;
public function getPaginationPersistenceAdapter(): ?PersistenceAdapterInterface;
- public function getPaginationPersistenceSubject(): ?PersistenceSubjectInterface;
+ public function getPaginationPersistenceSubjectProvider(): ?PersistenceSubjectProviderInterface;
public function getDefaultPaginationData(): ?PaginationData;
@@ -135,8 +105,16 @@ public function getRequestHandler(): ?RequestHandlerInterface;
public function getHeaderRowAttributes(): array;
+ public function hasHeaderRowAttribute(string $name): bool;
+
+ public function getHeaderRowAttribute(string $name, mixed $default = null): mixed;
+
public function getValueRowAttributes(): array;
+ public function hasValueRowAttribute(string $name): bool;
+
+ public function getValueRowAttribute(string $name, mixed $default = null): mixed;
+
public function getPageParameterName(): string;
public function getPerPageParameterName(): string;
diff --git a/src/DataTableFactory.php b/src/DataTableFactory.php
old mode 100644
new mode 100755
index e2cf5a6c..3cebbf1f
--- a/src/DataTableFactory.php
+++ b/src/DataTableFactory.php
@@ -4,6 +4,7 @@
namespace Kreyu\Bundle\DataTableBundle;
+use Kreyu\Bundle\DataTableBundle\Exception\InvalidArgumentException;
use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryFactoryInterface;
use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
use Kreyu\Bundle\DataTableBundle\Type\DataTableType;
@@ -12,7 +13,7 @@ class DataTableFactory implements DataTableFactoryInterface
{
public function __construct(
private DataTableRegistryInterface $registry,
- private ProxyQueryFactoryInterface $proxyQueryFactory,
+ private ?ProxyQueryFactoryInterface $proxyQueryFactory = null,
) {
}
@@ -34,6 +35,10 @@ public function createBuilder(string $type = DataTableType::class, mixed $query
public function createNamedBuilder(string $name, string $type = DataTableType::class, mixed $query = null, array $options = []): DataTableBuilderInterface
{
if (null !== $query && !$query instanceof ProxyQueryInterface) {
+ if (null === $this->proxyQueryFactory) {
+ throw new InvalidArgumentException(sprintf('Expected query of type %s, %s given', ProxyQueryInterface::class, get_debug_type($query)));
+ }
+
$query = $this->proxyQueryFactory->create($query);
}
diff --git a/src/DataTableFactoryAwareTrait.php b/src/DataTableFactoryAwareTrait.php
old mode 100644
new mode 100755
index 917bc76c..9bed95ac
--- a/src/DataTableFactoryAwareTrait.php
+++ b/src/DataTableFactoryAwareTrait.php
@@ -4,7 +4,6 @@
namespace Kreyu\Bundle\DataTableBundle;
-use JetBrains\PhpStorm\ArrayShape;
use Kreyu\Bundle\DataTableBundle\Type\DataTableType;
use Kreyu\Bundle\DataTableBundle\Type\DataTableTypeInterface;
use Symfony\Contracts\Service\Attribute\Required;
@@ -22,7 +21,7 @@ public function setDataTableFactory(?DataTableFactoryInterface $dataTableFactory
/**
* @param class-string $type
*/
- protected function createDataTable(string $type, mixed $query = null, #[ArrayShape(DataTableType::DEFAULT_OPTIONS)] array $options = []): DataTableInterface
+ protected function createDataTable(string $type, mixed $query = null, array $options = []): DataTableInterface
{
if (null === $this->dataTableFactory) {
throw new \LogicException(sprintf('You cannot use the "%s" method on controller without data table factory.', __METHOD__));
@@ -34,7 +33,7 @@ protected function createDataTable(string $type, mixed $query = null, #[ArraySha
/**
* @param class-string $type
*/
- protected function createNamedDataTable(string $name, string $type, mixed $query = null, #[ArrayShape(DataTableType::DEFAULT_OPTIONS)] array $options = []): DataTableInterface
+ protected function createNamedDataTable(string $name, string $type, mixed $query = null, array $options = []): DataTableInterface
{
if (null === $this->dataTableFactory) {
throw new \LogicException(sprintf('You cannot use the "%s" method on controller without data table factory.', __METHOD__));
@@ -43,7 +42,7 @@ protected function createNamedDataTable(string $name, string $type, mixed $query
return $this->dataTableFactory->createNamed($name, $type, $query, $options);
}
- protected function createDataTableBuilder(mixed $query = null, #[ArrayShape(DataTableType::DEFAULT_OPTIONS)] array $options = []): DataTableBuilderInterface
+ protected function createDataTableBuilder(mixed $query = null, array $options = []): DataTableBuilderInterface
{
if (null === $this->dataTableFactory) {
throw new \LogicException(sprintf('You cannot use the "%s" method on controller without data table factory.', __METHOD__));
diff --git a/src/DataTableFactoryBuilder.php b/src/DataTableFactoryBuilder.php
new file mode 100644
index 00000000..37a7d998
--- /dev/null
+++ b/src/DataTableFactoryBuilder.php
@@ -0,0 +1,97 @@
+resolvedTypeFactory = $resolvedTypeFactory;
+
+ return $this;
+ }
+
+ public function addExtension(DataTableExtensionInterface $extension): static
+ {
+ $this->extensions[] = $extension;
+
+ return $this;
+ }
+
+ public function addExtensions(array $extensions): static
+ {
+ $this->extensions = array_merge($this->extensions, $extensions);
+
+ return $this;
+ }
+
+ public function addType(DataTableTypeInterface $type): static
+ {
+ $this->types[] = $type;
+
+ return $this;
+ }
+
+ public function addTypes(array $types): static
+ {
+ foreach ($types as $type) {
+ $this->types[] = $type;
+ }
+
+ return $this;
+ }
+
+ public function addTypeExtension(DataTableTypeExtensionInterface $typeExtension): static
+ {
+ foreach ($typeExtension::getExtendedTypes() as $extendedType) {
+ $this->typeExtensions[$extendedType][] = $typeExtension;
+ }
+
+ return $this;
+ }
+
+ public function addTypeExtensions(array $typeExtensions): static
+ {
+ foreach ($typeExtensions as $typeExtension) {
+ $this->addTypeExtension($typeExtension);
+ }
+
+ return $this;
+ }
+
+ public function setProxyQueryFactory(?ProxyQueryFactoryInterface $proxyQueryFactory): static
+ {
+ $this->proxyQueryFactory = $proxyQueryFactory;
+
+ return $this;
+ }
+
+ public function getDataTableFactory(): DataTableFactoryInterface
+ {
+ $extensions = $this->extensions;
+
+ if (\count($this->types) > 0 || \count($this->typeExtensions) > 0) {
+ $extensions[] = new PreloadedDataTableExtension($this->types, $this->typeExtensions);
+ }
+
+ $registry = new DataTableRegistry($extensions, $this->resolvedTypeFactory ?? new ResolvedDataTableTypeFactory());
+
+ return new DataTableFactory($registry, $this->proxyQueryFactory);
+ }
+}
diff --git a/src/DataTableFactoryBuilderInterface.php b/src/DataTableFactoryBuilderInterface.php
index 9b53081d..da6a5485 100644
--- a/src/DataTableFactoryBuilderInterface.php
+++ b/src/DataTableFactoryBuilderInterface.php
@@ -4,6 +4,38 @@
namespace Kreyu\Bundle\DataTableBundle;
+use Kreyu\Bundle\DataTableBundle\Extension\DataTableExtensionInterface;
+use Kreyu\Bundle\DataTableBundle\Extension\DataTableTypeExtensionInterface;
+use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryFactoryInterface;
+use Kreyu\Bundle\DataTableBundle\Type\DataTableTypeInterface;
+use Kreyu\Bundle\DataTableBundle\Type\ResolvedDataTableTypeFactoryInterface;
+
interface DataTableFactoryBuilderInterface
{
+ public function setResolvedTypeFactory(ResolvedDataTableTypeFactoryInterface $resolvedTypeFactory): static;
+
+ public function setProxyQueryFactory(?ProxyQueryFactoryInterface $proxyQueryFactory): static;
+
+ public function addExtension(DataTableExtensionInterface $extension): static;
+
+ /**
+ * @param array $extensions
+ */
+ public function addExtensions(array $extensions): static;
+
+ public function addType(DataTableTypeInterface $type): static;
+
+ /**
+ * @param array $types
+ */
+ public function addTypes(array $types): static;
+
+ public function addTypeExtension(DataTableTypeExtensionInterface $typeExtension): static;
+
+ /**
+ * @param array $typeExtensions
+ */
+ public function addTypeExtensions(array $typeExtensions): static;
+
+ public function getDataTableFactory(): DataTableFactoryInterface;
}
diff --git a/src/DataTableFactoryInterface.php b/src/DataTableFactoryInterface.php
old mode 100644
new mode 100755
diff --git a/src/DataTableInterface.php b/src/DataTableInterface.php
old mode 100644
new mode 100755
index 7b399fce..cda750e1
--- a/src/DataTableInterface.php
+++ b/src/DataTableInterface.php
@@ -5,10 +5,21 @@
namespace Kreyu\Bundle\DataTableBundle;
use Kreyu\Bundle\DataTableBundle\Action\ActionInterface;
+use Kreyu\Bundle\DataTableBundle\Action\Type\ActionType;
+use Kreyu\Bundle\DataTableBundle\Action\Type\ActionTypeInterface;
+use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface;
+use Kreyu\Bundle\DataTableBundle\Column\Type\ColumnType;
+use Kreyu\Bundle\DataTableBundle\Column\Type\ColumnTypeInterface;
use Kreyu\Bundle\DataTableBundle\Exception\OutOfBoundsException;
use Kreyu\Bundle\DataTableBundle\Exporter\ExportData;
+use Kreyu\Bundle\DataTableBundle\Exporter\ExporterInterface;
use Kreyu\Bundle\DataTableBundle\Exporter\ExportFile;
+use Kreyu\Bundle\DataTableBundle\Exporter\Type\ExporterType;
+use Kreyu\Bundle\DataTableBundle\Exporter\Type\ExporterTypeInterface;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FiltrationData;
+use Kreyu\Bundle\DataTableBundle\Filter\Type\FilterType;
+use Kreyu\Bundle\DataTableBundle\Filter\Type\FilterTypeInterface;
use Kreyu\Bundle\DataTableBundle\Pagination\PaginationData;
use Kreyu\Bundle\DataTableBundle\Pagination\PaginationInterface;
use Kreyu\Bundle\DataTableBundle\Personalization\PersonalizationData;
@@ -20,10 +31,65 @@ interface DataTableInterface
{
public function initialize(): void;
+ public function getName(): string;
+
public function getQuery(): ProxyQueryInterface;
public function getConfig(): DataTableConfigInterface;
+ /**
+ * @return array
+ */
+ public function getColumns(): array;
+
+ /**
+ * @return array
+ */
+ public function getVisibleColumns(): array;
+
+ /**
+ * @return array
+ */
+ public function getHiddenColumns(): array;
+
+ /**
+ * @return array
+ */
+ public function getExportableColumns(): array;
+
+ /**
+ * @throws OutOfBoundsException if column of given name does not exist
+ */
+ public function getColumn(string $name): ColumnInterface;
+
+ public function hasColumn(string $name): bool;
+
+ /**
+ * @param class-string $type
+ */
+ public function addColumn(ColumnInterface|string $column, string $type = ColumnType::class, array $options = []): static;
+
+ public function removeColumn(string $name): static;
+
+ /**
+ * @return array
+ */
+ public function getFilters(): array;
+
+ /**
+ * @throws OutOfBoundsException if filter of given name does not exist
+ */
+ public function getFilter(string $name): FilterInterface;
+
+ public function hasFilter(string $name): bool;
+
+ /**
+ * @param class-string $type
+ */
+ public function addFilter(FilterInterface|string $filter, string $type = FilterType::class, array $options = []): static;
+
+ public function removeFilter(string $name): static;
+
/**
* @return array
*/
@@ -36,7 +102,10 @@ public function getAction(string $name): ActionInterface;
public function hasAction(string $name): bool;
- public function addAction(ActionInterface|string $action, string $type = null, array $options = []): static;
+ /**
+ * @param class-string $type
+ */
+ public function addAction(ActionInterface|string $action, string $type = ActionType::class, array $options = []): static;
public function removeAction(string $name): static;
@@ -52,7 +121,10 @@ public function getBatchAction(string $name): ActionInterface;
public function hasBatchAction(string $name): bool;
- public function addBatchAction(ActionInterface|string $action, string $type = null, array $options = []): static;
+ /**
+ * @param class-string $type
+ */
+ public function addBatchAction(ActionInterface|string $action, string $type = ActionType::class, array $options = []): static;
public function removeBatchAction(string $name): static;
@@ -68,10 +140,32 @@ public function getRowAction(string $name): ActionInterface;
public function hasRowAction(string $name): bool;
- public function addRowAction(ActionInterface|string $action, string $type = null, array $options = []): static;
+ /**
+ * @param class-string $type
+ */
+ public function addRowAction(ActionInterface|string $action, string $type = ActionType::class, array $options = []): static;
public function removeRowAction(string $name): static;
+ /**
+ * @return array
+ */
+ public function getExporters(): array;
+
+ /**
+ * @throws OutOfBoundsException if exporter of given name does not exist
+ */
+ public function getExporter(string $name): ExporterInterface;
+
+ public function hasExporter(string $name): bool;
+
+ /**
+ * @param class-string $type
+ */
+ public function addExporter(ExporterInterface|string $exporter, string $type = ExporterType::class, array $options = []): static;
+
+ public function removeExporter(string $name): static;
+
public function sort(SortingData $data): void;
public function filter(FiltrationData $data): void;
@@ -82,15 +176,27 @@ public function personalize(PersonalizationData $data): void;
public function export(ExportData $data = null): ExportFile;
+ public function getItems(): iterable;
+
public function getPagination(): PaginationInterface;
- public function getSortingData(): SortingData;
+ public function getSortingData(): ?SortingData;
+
+ public function setSortingData(?SortingData $sortingData): static;
+
+ public function setPaginationData(?PaginationData $paginationData): static;
+
+ public function getPaginationData(): ?PaginationData;
- public function getPaginationData(): PaginationData;
+ public function setFiltrationData(?FiltrationData $filtrationData): static;
public function getFiltrationData(): ?FiltrationData;
- public function getPersonalizationData(): PersonalizationData;
+ public function setPersonalizationData(?PersonalizationData $personalizationData): static;
+
+ public function getPersonalizationData(): ?PersonalizationData;
+
+ public function setExportData(?ExportData $exportData): static;
public function getExportData(): ?ExportData;
@@ -98,7 +204,7 @@ public function createFiltrationFormBuilder(DataTableView $view = null): FormBui
public function createPersonalizationFormBuilder(DataTableView $view = null): FormBuilderInterface;
- public function createExportFormBuilder(): FormBuilderInterface;
+ public function createExportFormBuilder(DataTableView $view = null): FormBuilderInterface;
public function isExporting(): bool;
@@ -107,4 +213,6 @@ public function hasActiveFilters(): bool;
public function handleRequest(mixed $request): void;
public function createView(): DataTableView;
+
+ public function createExportView(): DataTableView;
}
diff --git a/src/DataTableRegistry.php b/src/DataTableRegistry.php
old mode 100644
new mode 100755
index 8415f4f9..f0a51eff
--- a/src/DataTableRegistry.php
+++ b/src/DataTableRegistry.php
@@ -4,110 +4,32 @@
namespace Kreyu\Bundle\DataTableBundle;
-use Kreyu\Bundle\DataTableBundle\Exception\UnexpectedTypeException;
-use Kreyu\Bundle\DataTableBundle\Extension\DataTableTypeExtensionInterface;
+use Kreyu\Bundle\DataTableBundle\Extension\DataTableExtensionInterface;
use Kreyu\Bundle\DataTableBundle\Type\DataTableTypeInterface;
-use Kreyu\Bundle\DataTableBundle\Type\ResolvedDataTableTypeFactoryInterface;
use Kreyu\Bundle\DataTableBundle\Type\ResolvedDataTableTypeInterface;
-class DataTableRegistry implements DataTableRegistryInterface
+/**
+ * @extends AbstractRegistry
+ */
+class DataTableRegistry extends AbstractRegistry implements DataTableRegistryInterface
{
- /**
- * @var array
- */
- private array $types = [];
-
- /**
- * @var array
- */
- private array $resolvedTypes = [];
-
- /**
- * @var array
- */
- private array $checkedTypes = [];
-
- /**
- * @var array
- */
- private array $typeExtensions = [];
-
- /**
- * @param iterable $types
- * @param iterable $typeExtensions
- */
- public function __construct(
- iterable $types,
- iterable $typeExtensions,
- private ResolvedDataTableTypeFactoryInterface $resolvedDataTableTypeFactory,
- ) {
- foreach ($types as $type) {
- if (!$type instanceof DataTableTypeInterface) {
- throw new UnexpectedTypeException($type, DataTableTypeInterface::class);
- }
-
- $this->types[$type::class] = $type;
- }
-
- foreach ($typeExtensions as $typeExtension) {
- if (!$typeExtension instanceof DataTableTypeExtensionInterface) {
- throw new UnexpectedTypeException($typeExtension, DataTableTypeExtensionInterface::class);
- }
-
- $this->typeExtensions[$typeExtension::class] = $typeExtension;
- }
- }
-
public function getType(string $name): ResolvedDataTableTypeInterface
{
- if (!isset($this->resolvedTypes[$name])) {
- if (!isset($this->types[$name])) {
- throw new \InvalidArgumentException(sprintf('Could not load type "%s".', $name));
- }
-
- $this->resolvedTypes[$name] = $this->resolveType($this->types[$name]);
- }
-
- return $this->resolvedTypes[$name];
+ return $this->doGetType($name);
}
- private function resolveType(DataTableTypeInterface $type): ResolvedDataTableTypeInterface
+ final protected function getErrorContextName(): string
{
- $fqcn = $type::class;
-
- if (isset($this->checkedTypes[$fqcn])) {
- $types = implode(' > ', array_merge(array_keys($this->checkedTypes), [$fqcn]));
- throw new \LogicException(sprintf('Circular reference detected for data table type "%s" (%s).', $fqcn, $types));
- }
-
- $this->checkedTypes[$fqcn] = true;
-
- $typeExtensions = array_filter(
- $this->typeExtensions,
- fn (DataTableTypeExtensionInterface $extension) => $this->isFqcnExtensionEligible($fqcn, $extension),
- );
-
- $parentType = $type->getParent();
-
- try {
- return $this->resolvedDataTableTypeFactory->createResolvedType(
- $type,
- $typeExtensions,
- $parentType ? $this->getType($parentType) : null,
- );
- } finally {
- unset($this->checkedTypes[$fqcn]);
- }
+ return 'data table';
}
- private function isFqcnExtensionEligible(string $fqcn, DataTableTypeExtensionInterface $extension): bool
+ final protected function getTypeClass(): string
{
- $extendedTypes = $extension::getExtendedTypes();
-
- if ($extendedTypes instanceof \Traversable) {
- $extendedTypes = iterator_to_array($extendedTypes);
- }
+ return DataTableTypeInterface::class;
+ }
- return in_array($fqcn, $extendedTypes);
+ final protected function getExtensionClass(): string
+ {
+ return DataTableExtensionInterface::class;
}
}
diff --git a/src/DataTableRegistryInterface.php b/src/DataTableRegistryInterface.php
old mode 100644
new mode 100755
diff --git a/src/DataTableView.php b/src/DataTableView.php
old mode 100644
new mode 100755
index 41889715..2fa5ea09
--- a/src/DataTableView.php
+++ b/src/DataTableView.php
@@ -12,16 +12,16 @@ class DataTableView
{
public array $vars = [];
- public HeaderRowView $headerRow;
+ public ?HeaderRowView $headerRow = null;
- public HeaderRowView $nonPersonalizedHeaderRow;
+ public ?HeaderRowView $nonPersonalizedHeaderRow = null;
/**
* @var iterable
*/
public iterable $valueRows = [];
- public PaginationView $pagination;
+ public ?PaginationView $pagination = null;
/**
* @var array
diff --git a/src/DataTables.php b/src/DataTables.php
new file mode 100644
index 00000000..57b0db1a
--- /dev/null
+++ b/src/DataTables.php
@@ -0,0 +1,75 @@
+getDataTableFactory();
+ }
+
+ public static function createColumnFactory(): ColumnFactoryInterface
+ {
+ return self::createColumnFactoryBuilder()->getColumnFactory();
+ }
+
+ public static function createFilterFactory(): FilterFactoryInterface
+ {
+ return self::createFilterFactoryBuilder()->getFilterFactory();
+ }
+
+ public static function createActionFactory(): ActionFactoryInterface
+ {
+ return self::createActionFactoryBuilder()->getActionFactory();
+ }
+
+ public static function createExporterFactory(): ExporterFactoryInterface
+ {
+ return self::createExporterFactoryBuilder()->getExporterFactory();
+ }
+
+ public static function createDataTableFactoryBuilder(): DataTableFactoryBuilderInterface
+ {
+ return new DataTableFactoryBuilder();
+ }
+
+ public static function createColumnFactoryBuilder(): ColumnFactoryBuilderInterface
+ {
+ return new ColumnFactoryBuilder();
+ }
+
+ public static function createFilterFactoryBuilder(): FilterFactoryBuilderInterface
+ {
+ return new FilterFactoryBuilder();
+ }
+
+ public static function createActionFactoryBuilder(): ActionFactoryBuilderInterface
+ {
+ return new ActionFactoryBuilder();
+ }
+
+ public static function createExporterFactoryBuilder(): ExporterFactoryBuilderInterface
+ {
+ return new ExporterFactoryBuilder();
+ }
+
+ private function __construct()
+ {
+ }
+}
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
old mode 100644
new mode 100755
index a3c5e79a..36aa1a82
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -40,8 +40,7 @@ public function getConfigTreeBuilder(): TreeBuilder
->children()
->arrayNode('themes')
->scalarPrototype()->end()
- // TODO: Uncomment after removing the root "themes" node
- // ->defaultValue(['@KreyuDataTable/themes/base.html.twig'])
+ ->defaultValue(['@KreyuDataTable/themes/base.html.twig'])
->end()
->scalarNode('column_factory')
->defaultValue('kreyu_data_table.column.factory')
diff --git a/src/DependencyInjection/DataTablePass.php b/src/DependencyInjection/DataTablePass.php
new file mode 100644
index 00000000..e26c0ffb
--- /dev/null
+++ b/src/DependencyInjection/DataTablePass.php
@@ -0,0 +1,92 @@
+extensions as $extensionId) {
+ $this->processExtension($container, $extensionId);
+ }
+ }
+
+ private function processExtension(ContainerBuilder $container, string $extensionId): void
+ {
+ if (!$container->hasDefinition($extensionId)) {
+ return;
+ }
+
+ $definition = $container->getDefinition($extensionId);
+ $attributes = $definition->getTag($extensionId)[0];
+
+ $definition->replaceArgument(0, $this->processTypes($container, $attributes['type']));
+ $definition->replaceArgument(1, $this->processTypeExtensions($container, $attributes['type_extension']));
+ }
+
+ private function processTypes(ContainerBuilder $container, string $tagName): Reference
+ {
+ $servicesMap = [];
+
+ foreach ($container->findTaggedServiceIds($tagName, true) as $serviceId => $reference) {
+ $serviceDefinition = $container->getDefinition($serviceId);
+ $servicesMap[$serviceDefinition->getClass()] = new Reference($serviceId);
+ }
+
+ return ServiceLocatorTagPass::register($container, $servicesMap);
+ }
+
+ private function processTypeExtensions(ContainerBuilder $container, string $tagName): array
+ {
+ $typeExtensions = [];
+
+ foreach ($this->findAndSortTaggedServices($tagName, $container) as $reference) {
+ $serviceId = (string) $reference;
+ $serviceDefinition = $container->getDefinition($serviceId);
+
+ $tag = $serviceDefinition->getTag($tagName);
+ $typeExtensionClass = $container->getParameterBag()->resolveValue($serviceDefinition->getClass());
+
+ if (isset($tag[0]['extended_type'])) {
+ $typeExtensions[$tag[0]['extended_type']][] = new Reference($serviceId);
+ } else {
+ $extendsTypes = false;
+
+ foreach ($typeExtensionClass::getExtendedTypes() as $extendedType) {
+ $typeExtensions[$extendedType][] = new Reference($serviceId);
+ $extendsTypes = true;
+ }
+
+ if (!$extendsTypes) {
+ throw new InvalidArgumentException(sprintf('The getExtendedTypes() method for service "%s" does not return any extended types.', $serviceId));
+ }
+ }
+ }
+
+ foreach ($typeExtensions as $extendedType => $extensions) {
+ $typeExtensions[$extendedType] = new IteratorArgument($extensions);
+ }
+
+ return $typeExtensions;
+ }
+}
diff --git a/src/DependencyInjection/DefaultConfigurationPass.php b/src/DependencyInjection/DefaultConfigurationPass.php
old mode 100644
new mode 100755
index 52982af2..827a2d93
--- a/src/DependencyInjection/DefaultConfigurationPass.php
+++ b/src/DependencyInjection/DefaultConfigurationPass.php
@@ -4,7 +4,7 @@
namespace Kreyu\Bundle\DataTableBundle\DependencyInjection;
-use Kreyu\Bundle\DataTableBundle\DataTableConfigInterface;
+use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceContext;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
@@ -15,11 +15,13 @@ class DefaultConfigurationPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
- $extension = $container->getDefinition('kreyu_data_table.type_extension.default_configuration');
+ $extension = $container->getDefinition('kreyu_data_table.type.data_table');
$defaults = $extension->getArgument('$defaults');
- foreach (DataTableConfigInterface::PERSISTENCE_CONTEXTS as $context) {
+ foreach (PersistenceContext::cases() as $context) {
+ $context = $context->value;
+
if (!$defaults[$context]['persistence_enabled']) {
continue;
}
diff --git a/src/DependencyInjection/KreyuDataTableExtension.php b/src/DependencyInjection/KreyuDataTableExtension.php
old mode 100644
new mode 100755
index 08af7268..5ddd578a
--- a/src/DependencyInjection/KreyuDataTableExtension.php
+++ b/src/DependencyInjection/KreyuDataTableExtension.php
@@ -4,12 +4,18 @@
namespace Kreyu\Bundle\DataTableBundle\DependencyInjection;
+use Kreyu\Bundle\DataTableBundle\Action\Extension\ActionExtensionInterface;
use Kreyu\Bundle\DataTableBundle\Action\Extension\ActionTypeExtensionInterface;
use Kreyu\Bundle\DataTableBundle\Action\Type\ActionTypeInterface;
+use Kreyu\Bundle\DataTableBundle\Column\Extension\ColumnExtensionInterface;
use Kreyu\Bundle\DataTableBundle\Column\Extension\ColumnTypeExtensionInterface;
use Kreyu\Bundle\DataTableBundle\Column\Type\ColumnTypeInterface;
+use Kreyu\Bundle\DataTableBundle\Exporter\Extension\ExporterExtensionInterface;
+use Kreyu\Bundle\DataTableBundle\Exporter\Extension\ExporterTypeExtensionInterface;
use Kreyu\Bundle\DataTableBundle\Exporter\Type\ExporterTypeInterface;
+use Kreyu\Bundle\DataTableBundle\Extension\DataTableExtensionInterface;
use Kreyu\Bundle\DataTableBundle\Extension\DataTableTypeExtensionInterface;
+use Kreyu\Bundle\DataTableBundle\Filter\Extension\FilterExtensionInterface;
use Kreyu\Bundle\DataTableBundle\Filter\Extension\FilterTypeExtensionInterface;
use Kreyu\Bundle\DataTableBundle\Filter\Type\FilterTypeInterface;
use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface;
@@ -24,6 +30,26 @@
class KreyuDataTableExtension extends Extension implements PrependExtensionInterface
{
+ private array $autoconfiguration = [
+ DataTableExtensionInterface::class => 'kreyu_data_table.extension',
+ DataTableTypeInterface::class => 'kreyu_data_table.type',
+ DataTableTypeExtensionInterface::class => 'kreyu_data_table.type_extension',
+ ColumnExtensionInterface::class => 'kreyu_data_table.column.extension',
+ ColumnTypeInterface::class => 'kreyu_data_table.column.type',
+ ColumnTypeExtensionInterface::class => 'kreyu_data_table.column.type_extension',
+ FilterExtensionInterface::class => 'kreyu_data_table.filter.extension',
+ FilterTypeInterface::class => 'kreyu_data_table.filter.type',
+ FilterTypeExtensionInterface::class => 'kreyu_data_table.filter.type_extension',
+ ActionExtensionInterface::class => 'kreyu_data_table.action.extension',
+ ActionTypeInterface::class => 'kreyu_data_table.action.type',
+ ActionTypeExtensionInterface::class => 'kreyu_data_table.action.type_extension',
+ ExporterExtensionInterface::class => 'kreyu_data_table.exporter.extension',
+ ExporterTypeInterface::class => 'kreyu_data_table.exporter.type',
+ ExporterTypeExtensionInterface::class => 'kreyu_data_table.exporter.type_extension',
+ PersistenceAdapterInterface::class => 'kreyu_data_table.persistence.adapter',
+ ProxyQueryFactoryInterface::class => 'kreyu_data_table.proxy_query.factory',
+ ];
+
/**
* @throws \Exception
*/
@@ -41,63 +67,12 @@ public function load(array $configs, ContainerBuilder $container): void
$config = $this->resolveConfiguration($configs, $container);
- $container
- ->registerForAutoconfiguration(DataTableTypeInterface::class)
- ->addTag('kreyu_data_table.type')
- ;
-
- $container
- ->registerForAutoconfiguration(DataTableTypeExtensionInterface::class)
- ->addTag('kreyu_data_table.type_extension')
- ;
-
- $container
- ->registerForAutoconfiguration(ColumnTypeInterface::class)
- ->addTag('kreyu_data_table.column.type')
- ;
-
- $container
- ->registerForAutoconfiguration(ColumnTypeExtensionInterface::class)
- ->addTag('kreyu_data_table.column.type_extension')
- ;
-
- $container
- ->registerForAutoconfiguration(FilterTypeInterface::class)
- ->addTag('kreyu_data_table.filter.type')
- ;
-
- $container
- ->registerForAutoconfiguration(FilterTypeExtensionInterface::class)
- ->addTag('kreyu_data_table.filter.type_extension')
- ;
-
- $container
- ->registerForAutoconfiguration(ActionTypeInterface::class)
- ->addTag('kreyu_data_table.action.type')
- ;
-
- $container
- ->registerForAutoconfiguration(ActionTypeExtensionInterface::class)
- ->addTag('kreyu_data_table.action.type_extension')
- ;
-
- $container
- ->registerForAutoconfiguration(ExporterTypeInterface::class)
- ->addTag('kreyu_data_table.exporter.type')
- ;
-
- $container
- ->registerForAutoconfiguration(PersistenceAdapterInterface::class)
- ->addTag('kreyu_data_table.persistence.adapter')
- ;
-
- $container
- ->registerForAutoconfiguration(ProxyQueryFactoryInterface::class)
- ->addTag('kreyu_data_table.proxy_query.factory')
- ;
+ foreach ($this->autoconfiguration as $interface => $tag) {
+ $container->registerForAutoconfiguration($interface)->addTag($tag);
+ }
$container
- ->getDefinition('kreyu_data_table.type_extension.default_configuration')
+ ->getDefinition('kreyu_data_table.type.data_table')
->setArgument('$defaults', $config['defaults'])
;
}
diff --git a/src/Event/DataTableEvent.php b/src/Event/DataTableEvent.php
new file mode 100755
index 00000000..85bdb7ce
--- /dev/null
+++ b/src/Event/DataTableEvent.php
@@ -0,0 +1,21 @@
+dataTable;
+ }
+}
diff --git a/src/Event/DataTableEvents.php b/src/Event/DataTableEvents.php
new file mode 100755
index 00000000..fbceec7d
--- /dev/null
+++ b/src/Event/DataTableEvents.php
@@ -0,0 +1,59 @@
+exportData;
+ }
+
+ public function setExportData(ExportData $exportData): void
+ {
+ $this->exportData = $exportData;
+ }
+}
diff --git a/src/Event/DataTableFiltrationEvent.php b/src/Event/DataTableFiltrationEvent.php
new file mode 100755
index 00000000..29882b60
--- /dev/null
+++ b/src/Event/DataTableFiltrationEvent.php
@@ -0,0 +1,28 @@
+filtrationData;
+ }
+
+ public function setFiltrationData(FiltrationData $filtrationData): void
+ {
+ $this->filtrationData = $filtrationData;
+ }
+}
diff --git a/src/Event/DataTablePaginationEvent.php b/src/Event/DataTablePaginationEvent.php
new file mode 100755
index 00000000..c2fb08bb
--- /dev/null
+++ b/src/Event/DataTablePaginationEvent.php
@@ -0,0 +1,28 @@
+paginationData;
+ }
+
+ public function setPaginationData(PaginationData $paginationData): void
+ {
+ $this->paginationData = $paginationData;
+ }
+}
diff --git a/src/Event/DataTablePersonalizationEvent.php b/src/Event/DataTablePersonalizationEvent.php
new file mode 100755
index 00000000..882c17a1
--- /dev/null
+++ b/src/Event/DataTablePersonalizationEvent.php
@@ -0,0 +1,28 @@
+personalizationData;
+ }
+
+ public function setPersonalizationData(PersonalizationData $personalizationData): void
+ {
+ $this->personalizationData = $personalizationData;
+ }
+}
diff --git a/src/Event/DataTableSortingEvent.php b/src/Event/DataTableSortingEvent.php
new file mode 100755
index 00000000..3f2f52d5
--- /dev/null
+++ b/src/Event/DataTableSortingEvent.php
@@ -0,0 +1,28 @@
+sortingData;
+ }
+
+ public function setSortingData(SortingData $sortingData): void
+ {
+ $this->sortingData = $sortingData;
+ }
+}
diff --git a/src/Exception/BadMethodCallException.php b/src/Exception/BadMethodCallException.php
old mode 100644
new mode 100755
diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php
old mode 100644
new mode 100755
diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php
old mode 100644
new mode 100755
diff --git a/src/Exception/LogicException.php b/src/Exception/LogicException.php
new file mode 100755
index 00000000..eb6ce35c
--- /dev/null
+++ b/src/Exception/LogicException.php
@@ -0,0 +1,9 @@
+setRequired('exporter')
->setDefaults([
- 'filename' => 'export',
- 'strategy' => ExportStrategy::INCLUDE_CURRENT_PAGE,
- 'include_personalization' => false,
+ 'filename' => null,
+ 'exporter' => null,
+ 'strategy' => ExportStrategy::IncludeCurrentPage,
+ 'include_personalization' => true,
])
- ->setAllowedTypes('exporter', ExporterInterface::class)
- ->setAllowedTypes('filename', 'string')
+ ->setAllowedTypes('exporter', ['null', 'string', ExporterInterface::class])
+ ->setAllowedTypes('filename', ['null', 'string'])
->setAllowedTypes('strategy', ['string', ExportStrategy::class])
->setAllowedTypes('include_personalization', 'bool')
- ->setNormalizer('strategy', function (Options $options, $value): ExportStrategy {
+ ->addNormalizer('strategy', function (Options $options, $value): ExportStrategy {
return $value instanceof ExportStrategy ? $value : ExportStrategy::from($value);
})
+ ->addNormalizer('exporter', function (Options $options, $value): ?string {
+ if ($value instanceof ExporterInterface) {
+ return $value->getName();
+ }
+
+ return $value;
+ })
;
$data = array_intersect_key($data, array_flip($resolver->getDefinedOptions()));
@@ -48,15 +55,8 @@ public static function fromArray(array $data): self
public static function fromDataTable(DataTableInterface $dataTable): self
{
- $exporters = $dataTable->getConfig()->getExporters();
-
- if (empty($exporters)) {
- throw new \LogicException('Unable to create export data from data table without exporters');
- }
-
$self = new self();
$self->filename = $dataTable->getConfig()->getName();
- $self->exporter = $exporters[array_key_first($exporters)];
return $self;
}
diff --git a/src/Exporter/ExportFile.php b/src/Exporter/ExportFile.php
old mode 100644
new mode 100755
index ee1e10ce..bb83b96f
--- a/src/Exporter/ExportFile.php
+++ b/src/Exporter/ExportFile.php
@@ -10,7 +10,7 @@ class ExportFile extends File
{
public function __construct(
string $path,
- private string $filename,
+ private ?string $filename = null,
) {
parent::__construct($path);
}
diff --git a/src/Exporter/ExportStrategy.php b/src/Exporter/ExportStrategy.php
old mode 100644
new mode 100755
index 13f2b52e..2302aaec
--- a/src/Exporter/ExportStrategy.php
+++ b/src/Exporter/ExportStrategy.php
@@ -6,6 +6,37 @@
enum ExportStrategy: string
{
- case INCLUDE_CURRENT_PAGE = 'include-current-page';
- case INCLUDE_ALL = 'include-all';
+ case IncludeCurrentPage = 'include-current-page';
+ case IncludeAll = 'include-all';
+
+ /**
+ * @deprecated since 0.14.0, use {@see ExportStrategy::IncludeCurrentPage} instead
+ */
+ case INCLUDE_CURRENT_PAGE = 'deprecated-include-current-page';
+
+ /**
+ * @deprecated since 0.14.0, use {@see ExportStrategy::IncludeAll} instead
+ */
+ case INCLUDE_ALL = 'deprecated-include-all';
+
+ public function getLabel(): string
+ {
+ // TODO: Remove deprecated cases labels
+ return match ($this) {
+ self::INCLUDE_CURRENT_PAGE, self::IncludeCurrentPage => 'Include current page',
+ self::INCLUDE_ALL, self::IncludeAll => 'Include all',
+ };
+ }
+
+ /**
+ * TODO: Remove this method after removing deprecated cases.
+ */
+ public function getNonDeprecatedCase(): self
+ {
+ return match ($this) {
+ self::INCLUDE_CURRENT_PAGE => self::IncludeCurrentPage,
+ self::INCLUDE_ALL => self::IncludeAll,
+ default => $this,
+ };
+ }
}
diff --git a/src/Exporter/Exporter.php b/src/Exporter/Exporter.php
index 26be62e8..b5397305 100644
--- a/src/Exporter/Exporter.php
+++ b/src/Exporter/Exporter.php
@@ -4,30 +4,47 @@
namespace Kreyu\Bundle\DataTableBundle\Exporter;
+use Kreyu\Bundle\DataTableBundle\DataTableInterface;
use Kreyu\Bundle\DataTableBundle\DataTableView;
-use Kreyu\Bundle\DataTableBundle\Exporter\Type\ResolvedExporterTypeInterface;
+use Kreyu\Bundle\DataTableBundle\Exception\BadMethodCallException;
class Exporter implements ExporterInterface
{
+ private ?DataTableInterface $dataTable = null;
+
public function __construct(
- private string $name,
- private ResolvedExporterTypeInterface $type,
- private array $options = [],
+ private readonly ExporterConfigInterface $config,
) {
}
public function getName(): string
{
- return $this->name;
+ return $this->config->getName();
}
- public function getOption(string $name, mixed $default = null): mixed
+ public function getConfig(): ExporterConfigInterface
{
- return $this->options[$name] ?? $default;
+ return $this->config;
+ }
+
+ public function getDataTable(): DataTableInterface
+ {
+ if (null === $this->dataTable) {
+ throw new BadMethodCallException('Exporter is not attached to any data table.');
+ }
+
+ return $this->dataTable;
+ }
+
+ public function setDataTable(DataTableInterface $dataTable): static
+ {
+ $this->dataTable = $dataTable;
+
+ return $this;
}
- public function export(DataTableView $view, string $filename): ExportFile
+ public function export(DataTableView $view, string $filename = 'export'): ExportFile
{
- return $this->type->getInnerType()->export($view, $filename, $this->options);
+ return $this->config->getType()->getInnerType()->export($view, $this, $filename, $this->config->getOptions());
}
}
diff --git a/src/Exporter/ExporterBuilder.php b/src/Exporter/ExporterBuilder.php
new file mode 100755
index 00000000..a1758281
--- /dev/null
+++ b/src/Exporter/ExporterBuilder.php
@@ -0,0 +1,24 @@
+locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ return new Exporter($this->getExporterConfig());
+ }
+
+ private function createBuilderLockedException(): BadMethodCallException
+ {
+ return new BadMethodCallException('FilterBuilder methods cannot be accessed anymore once the builder is turned into a FilterConfigInterface instance.');
+ }
+}
diff --git a/src/Exporter/ExporterBuilderInterface.php b/src/Exporter/ExporterBuilderInterface.php
new file mode 100755
index 00000000..04c1a392
--- /dev/null
+++ b/src/Exporter/ExporterBuilderInterface.php
@@ -0,0 +1,10 @@
+name;
+ }
+
+ public function setName(string $name): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->name = $name;
+
+ return $this;
+ }
+
+ public function getType(): ResolvedExporterTypeInterface
+ {
+ return $this->type;
+ }
+
+ public function setType(ResolvedExporterTypeInterface $type): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->type = $type;
+
+ return $this;
+ }
+
+ public function getOptions(): array
+ {
+ return $this->options;
+ }
+
+ public function hasOption(string $name): bool
+ {
+ return array_key_exists($name, $this->options);
+ }
+
+ public function getOption(string $name, mixed $default = null): mixed
+ {
+ return $this->options[$name] ?? $default;
+ }
+
+ public function setOptions(array $options): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->options = $options;
+
+ return $this;
+ }
+
+ public function setOption(string $name, mixed $value): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->options[$name] = $value;
+
+ return $this;
+ }
+
+ public function getExporterConfig(): ExporterConfigInterface
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $config = clone $this;
+ $config->locked = true;
+
+ return $config;
+ }
+
+ private function createBuilderLockedException(): BadMethodCallException
+ {
+ return new BadMethodCallException('ExporterConfigBuilder methods cannot be accessed anymore once the builder is turned into a ExporterConfigInterface instance.');
+ }
+}
diff --git a/src/Exporter/ExporterConfigBuilderInterface.php b/src/Exporter/ExporterConfigBuilderInterface.php
new file mode 100755
index 00000000..bdb629c9
--- /dev/null
+++ b/src/Exporter/ExporterConfigBuilderInterface.php
@@ -0,0 +1,29 @@
+createBuilder($type, $options)->getExporter();
+ }
+
+ public function createNamed(string $name, string $type = ExporterType::class, array $options = []): ExporterInterface
+ {
+ return $this->createNamedBuilder($name, $type, $options)->getExporter();
+ }
+
+ public function createBuilder(string $type = ExporterType::class, array $options = []): ExporterBuilderInterface
+ {
+ return $this->createNamedBuilder($this->registry->getType($type)->getName(), $type, $options);
+ }
+
+ public function createNamedBuilder(string $name, string $type = ExporterType::class, array $options = []): ExporterBuilderInterface
{
$type = $this->registry->getType($type);
- $optionsResolver = $type->getOptionsResolver();
+ $builder = $type->createBuilder($this, $name, $options);
+
+ $type->buildExporter($builder, $builder->getOptions());
- return new Exporter($name, $type, $optionsResolver->resolve($options));
+ return $builder;
}
}
diff --git a/src/Exporter/ExporterFactoryBuilder.php b/src/Exporter/ExporterFactoryBuilder.php
new file mode 100644
index 00000000..cb7de3b8
--- /dev/null
+++ b/src/Exporter/ExporterFactoryBuilder.php
@@ -0,0 +1,88 @@
+resolvedTypeFactory = $resolvedTypeFactory;
+
+ return $this;
+ }
+
+ public function addExtension(ExporterExtensionInterface $extension): static
+ {
+ $this->extensions[] = $extension;
+
+ return $this;
+ }
+
+ public function addExtensions(array $extensions): static
+ {
+ $this->extensions = array_merge($this->extensions, $extensions);
+
+ return $this;
+ }
+
+ public function addType(ExporterTypeInterface $type): static
+ {
+ $this->types[] = $type;
+
+ return $this;
+ }
+
+ public function addTypes(array $types): static
+ {
+ foreach ($types as $type) {
+ $this->types[] = $type;
+ }
+
+ return $this;
+ }
+
+ public function addTypeExtension(ExporterTypeExtensionInterface $typeExtension): static
+ {
+ foreach ($typeExtension::getExtendedTypes() as $extendedType) {
+ $this->typeExtensions[$extendedType][] = $typeExtension;
+ }
+
+ return $this;
+ }
+
+ public function addTypeExtensions(array $typeExtensions): static
+ {
+ foreach ($typeExtensions as $typeExtension) {
+ $this->addTypeExtension($typeExtension);
+ }
+
+ return $this;
+ }
+
+ public function getExporterFactory(): ExporterFactoryInterface
+ {
+ $extensions = $this->extensions;
+
+ if (\count($this->types) > 0 || \count($this->typeExtensions) > 0) {
+ $extensions[] = new PreloadedExporterExtension($this->types, $this->typeExtensions);
+ }
+
+ $registry = new ExporterRegistry($extensions, $this->resolvedTypeFactory ?? new ResolvedExporterTypeFactory());
+
+ return new ExporterFactory($registry);
+ }
+}
diff --git a/src/Exporter/ExporterFactoryBuilderInterface.php b/src/Exporter/ExporterFactoryBuilderInterface.php
new file mode 100644
index 00000000..9c5ddf62
--- /dev/null
+++ b/src/Exporter/ExporterFactoryBuilderInterface.php
@@ -0,0 +1,38 @@
+ $extensions
+ */
+ public function addExtensions(array $extensions): static;
+
+ public function addType(ExporterTypeInterface $type): static;
+
+ /**
+ * @param array $types
+ */
+ public function addTypes(array $types): static;
+
+ public function addTypeExtension(ExporterTypeExtensionInterface $typeExtension): static;
+
+ /**
+ * @param array $typeExtensions
+ */
+ public function addTypeExtensions(array $typeExtensions): static;
+
+ public function getExporterFactory(): ExporterFactoryInterface;
+}
diff --git a/src/Exporter/ExporterFactoryInterface.php b/src/Exporter/ExporterFactoryInterface.php
old mode 100644
new mode 100755
index c998f95e..55f33022
--- a/src/Exporter/ExporterFactoryInterface.php
+++ b/src/Exporter/ExporterFactoryInterface.php
@@ -4,12 +4,37 @@
namespace Kreyu\Bundle\DataTableBundle\Exporter;
+use Kreyu\Bundle\DataTableBundle\Exporter\Type\ExporterType;
use Kreyu\Bundle\DataTableBundle\Exporter\Type\ExporterTypeInterface;
+use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
interface ExporterFactoryInterface
{
/**
* @param class-string $type
+ *
+ * @throws InvalidOptionsException if any of given option is not applicable to the given type
*/
- public function create(string $name, string $type, array $options = []): ExporterInterface;
+ public function create(string $type = ExporterType::class, array $options = []): ExporterInterface;
+
+ /**
+ * @param class-string $type
+ *
+ * @throws InvalidOptionsException if any of given option is not applicable to the given type
+ */
+ public function createNamed(string $name, string $type = ExporterType::class, array $options = []): ExporterInterface;
+
+ /**
+ * @param class-string $type
+ *
+ * @throws InvalidOptionsException if any of given option is not applicable to the given type
+ */
+ public function createBuilder(string $type = ExporterType::class, array $options = []): ExporterBuilderInterface;
+
+ /**
+ * @param class-string $type
+ *
+ * @throws InvalidOptionsException if any of given option is not applicable to the given type
+ */
+ public function createNamedBuilder(string $name, string $type = ExporterType::class, array $options = []): ExporterBuilderInterface;
}
diff --git a/src/Exporter/ExporterInterface.php b/src/Exporter/ExporterInterface.php
index 1d4edbf8..78994f99 100644
--- a/src/Exporter/ExporterInterface.php
+++ b/src/Exporter/ExporterInterface.php
@@ -4,13 +4,18 @@
namespace Kreyu\Bundle\DataTableBundle\Exporter;
+use Kreyu\Bundle\DataTableBundle\DataTableInterface;
use Kreyu\Bundle\DataTableBundle\DataTableView;
interface ExporterInterface
{
public function getName(): string;
- public function getOption(string $name, mixed $default = null): mixed;
+ public function getConfig(): ExporterConfigInterface;
- public function export(DataTableView $view, string $filename): ExportFile;
+ public function getDataTable(): DataTableInterface;
+
+ public function setDataTable(DataTableInterface $dataTable): static;
+
+ public function export(DataTableView $view, string $filename = 'export'): ExportFile;
}
diff --git a/src/Exporter/ExporterRegistry.php b/src/Exporter/ExporterRegistry.php
old mode 100644
new mode 100755
index daca316e..766c87be
--- a/src/Exporter/ExporterRegistry.php
+++ b/src/Exporter/ExporterRegistry.php
@@ -4,77 +4,33 @@
namespace Kreyu\Bundle\DataTableBundle\Exporter;
-use Kreyu\Bundle\DataTableBundle\Exception\UnexpectedTypeException;
+use Kreyu\Bundle\DataTableBundle\AbstractRegistry;
+use Kreyu\Bundle\DataTableBundle\Exporter\Extension\ExporterExtensionInterface;
use Kreyu\Bundle\DataTableBundle\Exporter\Type\ExporterTypeInterface;
-use Kreyu\Bundle\DataTableBundle\Exporter\Type\ResolvedExporterTypeFactoryInterface;
use Kreyu\Bundle\DataTableBundle\Exporter\Type\ResolvedExporterTypeInterface;
-class ExporterRegistry implements ExporterRegistryInterface
+/**
+ * @extends AbstractRegistry
+ */
+class ExporterRegistry extends AbstractRegistry implements ExporterRegistryInterface
{
- /**
- * @var array
- */
- private array $types = [];
-
- /**
- * @var array
- */
- private array $resolvedTypes = [];
-
- /**
- * @var array
- */
- private array $checkedTypes = [];
-
- /**
- * @param array $types
- */
- public function __construct(
- iterable $types,
- private ResolvedExporterTypeFactoryInterface $resolvedExporterTypeFactory,
- ) {
- foreach ($types as $type) {
- if (!$type instanceof ExporterTypeInterface) {
- throw new UnexpectedTypeException($type, ExporterTypeInterface::class);
- }
-
- $this->types[$type::class] = $type;
- }
- }
-
public function getType(string $name): ResolvedExporterTypeInterface
{
- if (!isset($this->resolvedTypes[$name])) {
- if (!isset($this->types[$name])) {
- throw new \InvalidArgumentException(sprintf('Could not load type "%s".', $name));
- }
-
- $this->resolvedTypes[$name] = $this->resolveType($this->types[$name]);
- }
-
- return $this->resolvedTypes[$name];
+ return $this->doGetType($name);
}
- private function resolveType(ExporterTypeInterface $type): ResolvedExporterTypeInterface
+ final protected function getErrorContextName(): string
{
- $fqcn = $type::class;
-
- if (isset($this->checkedTypes[$fqcn])) {
- $types = implode(' > ', array_merge(array_keys($this->checkedTypes), [$fqcn]));
- throw new \LogicException(sprintf('Circular reference detected for filter type "%s" (%s).', $fqcn, $types));
- }
-
- $this->checkedTypes[$fqcn] = true;
+ return 'exporter';
+ }
- $parentType = $type->getParent();
+ final protected function getTypeClass(): string
+ {
+ return ExporterTypeInterface::class;
+ }
- try {
- return $this->resolvedExporterTypeFactory->createResolvedType(
- $type,
- $parentType ? $this->getType($parentType) : null,
- );
- } finally {
- unset($this->checkedTypes[$fqcn]);
- }
+ final protected function getExtensionClass(): string
+ {
+ return ExporterExtensionInterface::class;
}
}
diff --git a/src/Exporter/ExporterRegistryInterface.php b/src/Exporter/ExporterRegistryInterface.php
old mode 100644
new mode 100755
diff --git a/src/Exporter/Extension/AbstractExporterExtension.php b/src/Exporter/Extension/AbstractExporterExtension.php
new file mode 100644
index 00000000..bfdd58fb
--- /dev/null
+++ b/src/Exporter/Extension/AbstractExporterExtension.php
@@ -0,0 +1,34 @@
+
+ */
+abstract class AbstractExporterExtension extends AbstractExtension implements ExporterExtensionInterface
+{
+ public function getType(string $name): ExporterTypeInterface
+ {
+ return $this->doGetType($name);
+ }
+
+ final protected function getErrorContextName(): string
+ {
+ return 'exporter';
+ }
+
+ final protected function getTypeClass(): string
+ {
+ return ExporterTypeInterface::class;
+ }
+
+ final protected function getTypeExtensionClass(): string
+ {
+ return ExporterTypeExtensionInterface::class;
+ }
+}
diff --git a/src/Exporter/Extension/AbstractExporterTypeExtension.php b/src/Exporter/Extension/AbstractExporterTypeExtension.php
new file mode 100755
index 00000000..ccfade42
--- /dev/null
+++ b/src/Exporter/Extension/AbstractExporterTypeExtension.php
@@ -0,0 +1,19 @@
+doGetType($name);
+ }
+
+ protected function getTypeClass(): string
+ {
+ return ExporterTypeInterface::class;
+ }
+
+ protected function getErrorContextName(): string
+ {
+ return 'filter';
+ }
+}
diff --git a/src/Exporter/Extension/ExporterExtensionInterface.php b/src/Exporter/Extension/ExporterExtensionInterface.php
new file mode 100644
index 00000000..3ddd4dd3
--- /dev/null
+++ b/src/Exporter/Extension/ExporterExtensionInterface.php
@@ -0,0 +1,18 @@
+>
+ */
+ public static function getExtendedTypes(): iterable;
+}
diff --git a/src/Exporter/Extension/PreloadedExporterExtension.php b/src/Exporter/Extension/PreloadedExporterExtension.php
new file mode 100644
index 00000000..f553c335
--- /dev/null
+++ b/src/Exporter/Extension/PreloadedExporterExtension.php
@@ -0,0 +1,31 @@
+ $types
+ * @param array> $typeExtensions
+ */
+ public function __construct(
+ private readonly array $types = [],
+ private readonly array $typeExtensions = [],
+ ) {
+ }
+
+ protected function loadTypes(): array
+ {
+ return $this->types;
+ }
+
+ protected function loadTypeExtensions(): array
+ {
+ return $this->typeExtensions;
+ }
+}
diff --git a/src/Exporter/Form/Type/ExportDataType.php b/src/Exporter/Form/Type/ExportDataType.php
old mode 100644
new mode 100755
index e5a4a710..767eab00
--- a/src/Exporter/Form/Type/ExportDataType.php
+++ b/src/Exporter/Form/Type/ExportDataType.php
@@ -6,11 +6,9 @@
use Kreyu\Bundle\DataTableBundle\Exporter\ExportData;
use Kreyu\Bundle\DataTableBundle\Exporter\ExporterInterface;
-use Kreyu\Bundle\DataTableBundle\Exporter\ExportStrategy;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
-use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -20,20 +18,14 @@ class ExportDataType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
- ->add('filename', TextType::class, [
- 'data' => $options['default_filename'],
- ])
+ ->add('filename', TextType::class)
->add('exporter', ChoiceType::class, [
'choices' => array_flip(array_map(
- fn (ExporterInterface $exporter) => $exporter->getOption('label', $exporter->getName()),
+ fn (ExporterInterface $exporter) => $exporter->getConfig()->getOption('label', $exporter->getName()),
$options['exporters'],
)),
- 'getter' => fn (ExportData $data) => $data->exporter->getName(),
- 'setter' => fn (ExportData $data, mixed $exporter) => $data->exporter = $options['exporters'][$exporter],
- ])
- ->add('strategy', EnumType::class, [
- 'class' => ExportStrategy::class,
])
+ ->add('strategy', ExportStrategyType::class)
->add('includePersonalization', CheckboxType::class, [
'required' => false,
])
@@ -45,11 +37,13 @@ public function configureOptions(OptionsResolver $resolver): void
$resolver->setDefaults([
'data_class' => ExportData::class,
'translation_domain' => 'KreyuDataTable',
- 'default_filename' => null,
'exporters' => [],
]);
$resolver->setAllowedTypes('exporters', ExporterInterface::class.'[]');
- $resolver->setAllowedTypes('default_filename', ['null', 'string']);
+
+ // TODO: Remove deprecated default filename option
+ $resolver->setDefault('default_filename', null);
+ $resolver->setDeprecated('default_filename', 'kreyu/data-table-bundle', '0.14', 'The "%name%" option is deprecated.');
}
}
diff --git a/src/Exporter/Form/Type/ExportStrategyType.php b/src/Exporter/Form/Type/ExportStrategyType.php
new file mode 100755
index 00000000..fc8298aa
--- /dev/null
+++ b/src/Exporter/Form/Type/ExportStrategyType.php
@@ -0,0 +1,32 @@
+setDefaults([
+ 'class' => ExportStrategy::class,
+ 'choice_translation_domain' => 'KreyuDataTable',
+ 'choice_label' => 'label',
+ // TODO: Remove after removing deprecated export strategy enum cases
+ 'choices' => [
+ ExportStrategy::IncludeCurrentPage,
+ ExportStrategy::IncludeAll,
+ ],
+ ]);
+ }
+
+ public function getParent(): string
+ {
+ return EnumType::class;
+ }
+}
diff --git a/src/Exporter/Type/AbstractExporterType.php b/src/Exporter/Type/AbstractExporterType.php
old mode 100644
new mode 100755
index 13689c53..dd94b1d4
--- a/src/Exporter/Type/AbstractExporterType.php
+++ b/src/Exporter/Type/AbstractExporterType.php
@@ -4,15 +4,26 @@
namespace Kreyu\Bundle\DataTableBundle\Exporter\Type;
+use Kreyu\Bundle\DataTableBundle\Exporter\ExporterBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Util\StringUtil;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\OptionsResolver\OptionsResolver;
abstract class AbstractExporterType implements ExporterTypeInterface
{
+ public function buildExporter(ExporterBuilderInterface $builder, array $options): void
+ {
+ }
+
public function configureOptions(OptionsResolver $resolver): void
{
}
+ public function getName(): string
+ {
+ return StringUtil::fqcnToShortName(static::class, ['ExporterType', 'Type']) ?: '';
+ }
+
public function getParent(): ?string
{
return ExporterType::class;
diff --git a/src/Exporter/Type/CallbackExporterType.php b/src/Exporter/Type/CallbackExporterType.php
new file mode 100755
index 00000000..34286ace
--- /dev/null
+++ b/src/Exporter/Type/CallbackExporterType.php
@@ -0,0 +1,26 @@
+setRequired('callback')
+ ->setAllowedTypes('callback', ['callable'])
+ ;
+ }
+}
diff --git a/src/Exporter/Type/ExporterType.php b/src/Exporter/Type/ExporterType.php
index 14cfd97a..b5758820 100644
--- a/src/Exporter/Type/ExporterType.php
+++ b/src/Exporter/Type/ExporterType.php
@@ -5,14 +5,21 @@
namespace Kreyu\Bundle\DataTableBundle\Exporter\Type;
use Kreyu\Bundle\DataTableBundle\DataTableView;
+use Kreyu\Bundle\DataTableBundle\Exception\LogicException;
+use Kreyu\Bundle\DataTableBundle\Exporter\ExporterBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Exporter\ExporterInterface;
use Kreyu\Bundle\DataTableBundle\Exporter\ExportFile;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class ExporterType implements ExporterTypeInterface
{
- public function export(DataTableView $view, string $filename, array $options = []): ExportFile
+ public function export(DataTableView $view, ExporterInterface $exporter, string $filename, array $options = []): ExportFile
+ {
+ throw new LogicException('Base exporter type cannot be called directly');
+ }
+
+ public function buildExporter(ExporterBuilderInterface $builder, array $options): void
{
- throw new \LogicException('Base exporter type cannot be called directly');
}
public function configureOptions(OptionsResolver $resolver): void
@@ -20,11 +27,16 @@ public function configureOptions(OptionsResolver $resolver): void
$resolver->setDefaults([
'use_headers' => true,
'label' => null,
- 'tempnam_dir' => '/tmp',
+ 'tempnam_dir' => sys_get_temp_dir(),
'tempnam_prefix' => 'exporter_',
]);
}
+ public function getName(): string
+ {
+ return 'exporter';
+ }
+
public function getParent(): ?string
{
return null;
diff --git a/src/Exporter/Type/ExporterTypeInterface.php b/src/Exporter/Type/ExporterTypeInterface.php
index 6a8bbd92..ff99c26e 100644
--- a/src/Exporter/Type/ExporterTypeInterface.php
+++ b/src/Exporter/Type/ExporterTypeInterface.php
@@ -5,15 +5,21 @@
namespace Kreyu\Bundle\DataTableBundle\Exporter\Type;
use Kreyu\Bundle\DataTableBundle\DataTableView;
+use Kreyu\Bundle\DataTableBundle\Exporter\ExporterBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Exporter\ExporterInterface;
use Kreyu\Bundle\DataTableBundle\Exporter\ExportFile;
use Symfony\Component\OptionsResolver\OptionsResolver;
interface ExporterTypeInterface
{
- public function export(DataTableView $view, string $filename, array $options = []): ExportFile;
+ public function export(DataTableView $view, ExporterInterface $exporter, string $filename, array $options = []): ExportFile;
+
+ public function buildExporter(ExporterBuilderInterface $builder, array $options): void;
public function configureOptions(OptionsResolver $resolver): void;
+ public function getName(): string;
+
/**
* @return class-string|null
*/
diff --git a/src/Exporter/Type/ResolvedExporterType.php b/src/Exporter/Type/ResolvedExporterType.php
old mode 100644
new mode 100755
index 1cec3064..006d1b30
--- a/src/Exporter/Type/ResolvedExporterType.php
+++ b/src/Exporter/Type/ResolvedExporterType.php
@@ -4,18 +4,32 @@
namespace Kreyu\Bundle\DataTableBundle\Exporter\Type;
+use Kreyu\Bundle\DataTableBundle\Exporter\ExporterBuilder;
+use Kreyu\Bundle\DataTableBundle\Exporter\ExporterBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Exporter\ExporterFactoryInterface;
+use Kreyu\Bundle\DataTableBundle\Exporter\Extension\ExporterTypeExtensionInterface;
+use Symfony\Component\OptionsResolver\Exception\ExceptionInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ResolvedExporterType implements ResolvedExporterTypeInterface
{
private OptionsResolver $optionsResolver;
+ /**
+ * @param array $typeExtensions
+ */
public function __construct(
- private ExporterTypeInterface $innerType,
- private ?ResolvedExporterTypeInterface $parent = null,
+ private readonly ExporterTypeInterface $innerType,
+ private readonly array $typeExtensions = [],
+ private readonly ?ResolvedExporterTypeInterface $parent = null,
) {
}
+ public function getName(): string
+ {
+ return $this->innerType->getName();
+ }
+
public function getParent(): ?ResolvedExporterTypeInterface
{
return $this->parent;
@@ -26,6 +40,36 @@ public function getInnerType(): ExporterTypeInterface
return $this->innerType;
}
+ public function getTypeExtensions(): array
+ {
+ return $this->typeExtensions;
+ }
+
+ /**
+ * @throws ExceptionInterface
+ */
+ public function createBuilder(ExporterFactoryInterface $factory, string $name, array $options): ExporterBuilderInterface
+ {
+ try {
+ $options = $this->getOptionsResolver()->resolve($options);
+ } catch (ExceptionInterface $exception) {
+ throw new $exception(sprintf('An error has occurred resolving the options of the exporter "%s": ', get_debug_type($this->getInnerType())).$exception->getMessage(), $exception->getCode(), $exception);
+ }
+
+ return new ExporterBuilder($name, $this, $options);
+ }
+
+ public function buildExporter(ExporterBuilderInterface $builder, array $options): void
+ {
+ $this->parent?->buildExporter($builder, $options);
+
+ $this->innerType->buildExporter($builder, $options);
+
+ foreach ($this->typeExtensions as $extension) {
+ $extension->buildExporter($builder, $options);
+ }
+ }
+
public function getOptionsResolver(): OptionsResolver
{
if (!isset($this->optionsResolver)) {
diff --git a/src/Exporter/Type/ResolvedExporterTypeFactory.php b/src/Exporter/Type/ResolvedExporterTypeFactory.php
old mode 100644
new mode 100755
index cccb8188..01eddbe3
--- a/src/Exporter/Type/ResolvedExporterTypeFactory.php
+++ b/src/Exporter/Type/ResolvedExporterTypeFactory.php
@@ -6,8 +6,8 @@
class ResolvedExporterTypeFactory implements ResolvedExporterTypeFactoryInterface
{
- public function createResolvedType(ExporterTypeInterface $type, ResolvedExporterTypeInterface $parent = null): ResolvedExporterTypeInterface
+ public function createResolvedType(ExporterTypeInterface $type, array $typeExtensions = [], ResolvedExporterTypeInterface $parent = null): ResolvedExporterTypeInterface
{
- return new ResolvedExporterType($type, $parent);
+ return new ResolvedExporterType($type, $typeExtensions, $parent);
}
}
diff --git a/src/Exporter/Type/ResolvedExporterTypeFactoryInterface.php b/src/Exporter/Type/ResolvedExporterTypeFactoryInterface.php
old mode 100644
new mode 100755
index 7d8fe45d..45216ae7
--- a/src/Exporter/Type/ResolvedExporterTypeFactoryInterface.php
+++ b/src/Exporter/Type/ResolvedExporterTypeFactoryInterface.php
@@ -4,7 +4,12 @@
namespace Kreyu\Bundle\DataTableBundle\Exporter\Type;
+use Kreyu\Bundle\DataTableBundle\Exporter\Extension\ExporterTypeExtensionInterface;
+
interface ResolvedExporterTypeFactoryInterface
{
- public function createResolvedType(ExporterTypeInterface $type, ResolvedExporterTypeInterface $parent = null): ResolvedExporterTypeInterface;
+ /**
+ * @param array $typeExtensions
+ */
+ public function createResolvedType(ExporterTypeInterface $type, array $typeExtensions = [], ResolvedExporterTypeInterface $parent = null): ResolvedExporterTypeInterface;
}
diff --git a/src/Exporter/Type/ResolvedExporterTypeInterface.php b/src/Exporter/Type/ResolvedExporterTypeInterface.php
old mode 100644
new mode 100755
index fae5b7e1..d9b3e131
--- a/src/Exporter/Type/ResolvedExporterTypeInterface.php
+++ b/src/Exporter/Type/ResolvedExporterTypeInterface.php
@@ -4,13 +4,27 @@
namespace Kreyu\Bundle\DataTableBundle\Exporter\Type;
+use Kreyu\Bundle\DataTableBundle\Exporter\ExporterBuilderInterface;
+use Kreyu\Bundle\DataTableBundle\Exporter\ExporterFactoryInterface;
+use Kreyu\Bundle\DataTableBundle\Exporter\Extension\ExporterTypeExtensionInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
interface ResolvedExporterTypeInterface
{
+ public function getName(): string;
+
public function getParent(): ?ResolvedExporterTypeInterface;
public function getInnerType(): ExporterTypeInterface;
+ /**
+ * @return array
+ */
+ public function getTypeExtensions(): array;
+
+ public function createBuilder(ExporterFactoryInterface $factory, string $name, array $options): ExporterBuilderInterface;
+
+ public function buildExporter(ExporterBuilderInterface $builder, array $options): void;
+
public function getOptionsResolver(): OptionsResolver;
}
diff --git a/src/Extension/AbstractDataTableExtension.php b/src/Extension/AbstractDataTableExtension.php
new file mode 100644
index 00000000..94b577f2
--- /dev/null
+++ b/src/Extension/AbstractDataTableExtension.php
@@ -0,0 +1,34 @@
+
+ */
+abstract class AbstractDataTableExtension extends AbstractExtension implements DataTableExtensionInterface
+{
+ public function getType(string $name): DataTableTypeInterface
+ {
+ return $this->doGetType($name);
+ }
+
+ final protected function getErrorContextName(): string
+ {
+ return 'data table';
+ }
+
+ final protected function getTypeClass(): string
+ {
+ return DataTableTypeInterface::class;
+ }
+
+ final protected function getTypeExtensionClass(): string
+ {
+ return DataTableTypeExtensionInterface::class;
+ }
+}
diff --git a/src/Extension/AbstractDataTableTypeExtension.php b/src/Extension/AbstractDataTableTypeExtension.php
old mode 100644
new mode 100755
diff --git a/src/Extension/Core/DefaultConfigurationDataTableTypeExtension.php b/src/Extension/Core/DefaultConfigurationDataTableTypeExtension.php
old mode 100644
new mode 100755
index 258c057b..f4ea58ec
--- a/src/Extension/Core/DefaultConfigurationDataTableTypeExtension.php
+++ b/src/Extension/Core/DefaultConfigurationDataTableTypeExtension.php
@@ -5,12 +5,12 @@
namespace Kreyu\Bundle\DataTableBundle\Extension\Core;
use Kreyu\Bundle\DataTableBundle\Extension\AbstractDataTableTypeExtension;
-use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectInterface;
-use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectNotFoundException;
-use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface;
use Kreyu\Bundle\DataTableBundle\Type\DataTableType;
use Symfony\Component\OptionsResolver\OptionsResolver;
+/**
+ * @deprecated since 0.14.0, this extension is not used and the default configuration is applied in the {@see DataTableType}
+ */
class DefaultConfigurationDataTableTypeExtension extends AbstractDataTableTypeExtension
{
public function __construct(
@@ -28,21 +28,21 @@ public function configureOptions(OptionsResolver $resolver): void
'sorting_enabled' => $this->defaults['sorting']['enabled'],
'sorting_persistence_enabled' => $this->defaults['sorting']['persistence_enabled'],
'sorting_persistence_adapter' => $this->defaults['sorting']['persistence_adapter'],
- 'sorting_persistence_subject' => $this->getPersistenceSubject($this->defaults['sorting']['persistence_subject_provider']),
+ 'sorting_persistence_subject_provider' => $this->defaults['sorting']['persistence_subject_provider'],
'pagination_enabled' => $this->defaults['pagination']['enabled'],
'pagination_persistence_enabled' => $this->defaults['pagination']['persistence_enabled'],
'pagination_persistence_adapter' => $this->defaults['pagination']['persistence_adapter'],
- 'pagination_persistence_subject' => $this->getPersistenceSubject($this->defaults['pagination']['persistence_subject_provider']),
+ 'pagination_persistence_subject_provider' => $this->defaults['pagination']['persistence_subject_provider'],
'filtration_enabled' => $this->defaults['filtration']['enabled'],
'filtration_persistence_enabled' => $this->defaults['filtration']['persistence_enabled'],
'filtration_persistence_adapter' => $this->defaults['filtration']['persistence_adapter'],
- 'filtration_persistence_subject' => $this->getPersistenceSubject($this->defaults['filtration']['persistence_subject_provider']),
+ 'filtration_persistence_subject_provider' => $this->defaults['filtration']['persistence_subject_provider'],
'filtration_form_factory' => $this->defaults['filtration']['form_factory'],
'filter_factory' => $this->defaults['filtration']['filter_factory'],
'personalization_enabled' => $this->defaults['personalization']['enabled'],
'personalization_persistence_enabled' => $this->defaults['personalization']['persistence_enabled'],
'personalization_persistence_adapter' => $this->defaults['personalization']['persistence_adapter'],
- 'personalization_persistence_subject' => $this->getPersistenceSubject($this->defaults['personalization']['persistence_subject_provider']),
+ 'personalization_persistence_subject_provider' => $this->defaults['personalization']['persistence_subject_provider'],
'personalization_form_factory' => $this->defaults['personalization']['form_factory'],
'exporting_enabled' => $this->defaults['exporting']['enabled'],
'exporting_form_factory' => $this->defaults['exporting']['form_factory'],
@@ -54,13 +54,4 @@ public static function getExtendedTypes(): iterable
{
return [DataTableType::class];
}
-
- private function getPersistenceSubject(?PersistenceSubjectProviderInterface $persistenceSubjectProvider): ?PersistenceSubjectInterface
- {
- try {
- return $persistenceSubjectProvider?->provide();
- } catch (PersistenceSubjectNotFoundException) {
- return null;
- }
- }
}
diff --git a/src/Extension/DataTableExtensionInterface.php b/src/Extension/DataTableExtensionInterface.php
new file mode 100644
index 00000000..e6b6a385
--- /dev/null
+++ b/src/Extension/DataTableExtensionInterface.php
@@ -0,0 +1,18 @@
+doGetType($name);
+ }
+
+ protected function getTypeClass(): string
+ {
+ return DataTableTypeInterface::class;
+ }
+
+ protected function getErrorContextName(): string
+ {
+ return 'data table';
+ }
+}
diff --git a/src/Extension/HttpFoundation/HttpFoundationDataTableExtension.php b/src/Extension/HttpFoundation/HttpFoundationDataTableExtension.php
new file mode 100644
index 00000000..4b47f21a
--- /dev/null
+++ b/src/Extension/HttpFoundation/HttpFoundationDataTableExtension.php
@@ -0,0 +1,17 @@
+setRequestHandler($this->requestHandler);
+ }
+
+ public static function getExtendedTypes(): iterable
+ {
+ return [DataTableType::class];
+ }
+}
diff --git a/src/Extension/PreloadedDataTableExtension.php b/src/Extension/PreloadedDataTableExtension.php
new file mode 100644
index 00000000..b281506a
--- /dev/null
+++ b/src/Extension/PreloadedDataTableExtension.php
@@ -0,0 +1,30 @@
+ $types
+ * @param array> $typeExtensions
+ */
+ public function __construct(
+ private readonly array $types = [],
+ private readonly array $typeExtensions = [],
+ ) {
+ }
+
+ protected function loadTypes(): array
+ {
+ return $this->types;
+ }
+
+ protected function loadTypeExtensions(): array
+ {
+ return $this->typeExtensions;
+ }
+}
diff --git a/src/Filter/AbstractFilter.php b/src/Filter/AbstractFilter.php
deleted file mode 100644
index 0a0153b3..00000000
--- a/src/Filter/AbstractFilter.php
+++ /dev/null
@@ -1,118 +0,0 @@
-name = $name;
-
- $this->configureOptions($optionsResolver = new OptionsResolver());
-
- $this->options = $optionsResolver->resolve($options);
- }
-
- protected function configureOptions(OptionsResolver $resolver): void
- {
- $resolver
- ->setDefaults([
- 'label' => $this->getName(),
- 'label_translation_parameters' => [],
- 'translation_domain' => 'KreyuDataTable',
- 'field_name' => $this->getName(),
- 'field_type' => TextType::class,
- 'field_options' => [],
- 'operator_type' => OperatorType::class,
- 'operator_options' => [
- 'visible' => false,
- 'choices' => $this->getSupportedOperators(),
- ],
- ])
- ->setAllowedTypes('label', ['string', TranslatableMessage::class])
- ->setAllowedTypes('field_name', ['string'])
- ->setAllowedTypes('field_type', ['string'])
- ->setAllowedTypes('field_options', ['array'])
- ->setAllowedTypes('operator_type', ['string'])
- ->setAllowedTypes('operator_options', ['array'])
- ;
- }
-
- public function getName(): string
- {
- return $this->name;
- }
-
- public function getFormName(): string
- {
- return str_replace('.', '__', $this->getName());
- }
-
- public function getOptions(): array
- {
- return $this->options;
- }
-
- public function getOption(string $key, mixed $default = null): mixed
- {
- return $this->options[$key] ?? $default;
- }
-
- public function getLabel(): string
- {
- return (string) $this->getOption('label');
- }
-
- public function getFieldName(): string
- {
- return $this->getOption('field_name');
- }
-
- public function getFieldType(): string
- {
- return $this->getOption('field_type');
- }
-
- public function getFieldOptions(): array
- {
- return $this->getOption('field_options');
- }
-
- public function getOperatorType(): string
- {
- return $this->getOption('operator_type');
- }
-
- public function getOperatorOptions(): array
- {
- return $this->getOption('operator_options') + [
- 'choices' => $this->getSupportedOperators(),
- ];
- }
-
- public function getFormOptions(): array
- {
- return [
- 'field_type' => $this->getFieldType(),
- 'field_options' => $this->getFieldOptions(),
- 'operator_type' => $this->getOperatorType(),
- 'operator_options' => $this->getOperatorOptions(),
- 'label' => $this->getLabel(),
- ];
- }
-
- /**
- * @return array
- */
- abstract protected function getSupportedOperators(): array;
-}
diff --git a/src/Filter/Extension/AbstractFilterExtension.php b/src/Filter/Extension/AbstractFilterExtension.php
new file mode 100644
index 00000000..ee4ee636
--- /dev/null
+++ b/src/Filter/Extension/AbstractFilterExtension.php
@@ -0,0 +1,34 @@
+
+ */
+abstract class AbstractFilterExtension extends AbstractExtension implements FilterExtensionInterface
+{
+ public function getType(string $name): FilterTypeInterface
+ {
+ return $this->doGetType($name);
+ }
+
+ final protected function getErrorContextName(): string
+ {
+ return 'filter';
+ }
+
+ final protected function getTypeClass(): string
+ {
+ return FilterTypeInterface::class;
+ }
+
+ final protected function getTypeExtensionClass(): string
+ {
+ return FilterTypeExtensionInterface::class;
+ }
+}
diff --git a/src/Filter/Extension/AbstractFilterTypeExtension.php b/src/Filter/Extension/AbstractFilterTypeExtension.php
old mode 100644
new mode 100755
index 215e337c..49582518
--- a/src/Filter/Extension/AbstractFilterTypeExtension.php
+++ b/src/Filter/Extension/AbstractFilterTypeExtension.php
@@ -4,6 +4,7 @@
namespace Kreyu\Bundle\DataTableBundle\Filter\Extension;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FilterView;
@@ -16,6 +17,10 @@ public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterf
{
}
+ public function buildFilter(FilterBuilderInterface $builder, array $options): void
+ {
+ }
+
public function buildView(FilterView $view, FilterInterface $filter, array $options): void
{
}
diff --git a/src/Filter/Extension/DependencyInjection/DependencyInjectionFilterExtension.php b/src/Filter/Extension/DependencyInjection/DependencyInjectionFilterExtension.php
new file mode 100644
index 00000000..51b92a0f
--- /dev/null
+++ b/src/Filter/Extension/DependencyInjection/DependencyInjectionFilterExtension.php
@@ -0,0 +1,27 @@
+doGetType($name);
+ }
+
+ protected function getTypeClass(): string
+ {
+ return FilterTypeInterface::class;
+ }
+
+ protected function getErrorContextName(): string
+ {
+ return 'filter';
+ }
+}
diff --git a/src/Filter/Extension/FilterExtensionInterface.php b/src/Filter/Extension/FilterExtensionInterface.php
new file mode 100644
index 00000000..89254897
--- /dev/null
+++ b/src/Filter/Extension/FilterExtensionInterface.php
@@ -0,0 +1,18 @@
+ $types
+ * @param array> $typeExtensions
+ */
+ public function __construct(
+ private readonly array $types = [],
+ private readonly array $typeExtensions = [],
+ ) {
+ }
+
+ protected function loadTypes(): array
+ {
+ return $this->types;
+ }
+
+ protected function loadTypeExtensions(): array
+ {
+ return $this->typeExtensions;
+ }
+}
diff --git a/src/Filter/Filter.php b/src/Filter/Filter.php
old mode 100644
new mode 100755
index eb1a5a60..e1b3d3cd
--- a/src/Filter/Filter.php
+++ b/src/Filter/Filter.php
@@ -4,27 +4,44 @@
namespace Kreyu\Bundle\DataTableBundle\Filter;
+use Kreyu\Bundle\DataTableBundle\DataTableInterface;
use Kreyu\Bundle\DataTableBundle\DataTableView;
-use Kreyu\Bundle\DataTableBundle\Filter\Type\ResolvedFilterTypeInterface;
+use Kreyu\Bundle\DataTableBundle\Exception\BadMethodCallException;
use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
class Filter implements FilterInterface
{
+ private ?DataTableInterface $dataTable = null;
+
public function __construct(
- private string $name,
- private ResolvedFilterTypeInterface $type,
- private array $options = [],
+ private readonly FilterConfigInterface $config,
) {
}
- public function apply(ProxyQueryInterface $query, FilterData $data): void
+ public function getName(): string
{
- $this->type->apply($query, $data, $this, $this->options);
+ return $this->config->getName();
}
- public function getName(): string
+ public function getConfig(): FilterConfigInterface
+ {
+ return $this->config;
+ }
+
+ public function getDataTable(): DataTableInterface
{
- return $this->name;
+ if (null === $this->dataTable) {
+ throw new BadMethodCallException('Filter is not attached to any data table.');
+ }
+
+ return $this->dataTable;
+ }
+
+ public function setDataTable(DataTableInterface $dataTable): static
+ {
+ $this->dataTable = $dataTable;
+
+ return $this;
}
public function getFormName(): string
@@ -35,38 +52,49 @@ public function getFormName(): string
public function getFormOptions(): array
{
return [
- 'field_type' => $this->getOption('field_type'),
- 'field_options' => $this->getOption('field_options'),
- 'operator_type' => $this->getOption('operator_type'),
- 'operator_options' => $this->getOption('operator_options'),
+ 'form_type' => $this->config->getFormType(),
+ 'form_options' => $this->config->getFormOptions(),
+ 'operator_form_type' => $this->config->getOperatorFormType(),
+ 'operator_form_options' => $this->config->getOperatorFormOptions(),
+ 'default_operator' => $this->config->getDefaultOperator(),
+ 'supported_operators' => $this->config->getSupportedOperators(),
+ 'operator_selectable' => $this->config->isOperatorSelectable(),
];
}
public function getQueryPath(): string
{
- return $this->options['query_path'] ?? $this->name;
+ return $this->config->getOption('query_path', $this->getName());
}
- public function getType(): ResolvedFilterTypeInterface
+ public function apply(ProxyQueryInterface $query = null, FilterData $data = null): void
{
- return $this->type;
- }
+ $query ??= $this->getDataTable()->getQuery();
- public function getOptions(): array
- {
- return $this->options;
- }
+ if (null === $query) {
+ $error = 'Unable to apply filter without a query.';
+ $error .= ' Either ensure the related data table has a query or pass one explicitly.';
- public function getOption(string $name): mixed
- {
- return $this->options[$name];
+ throw new BadMethodCallException($error);
+ }
+
+ $data ??= $this->getDataTable()->getFiltrationData()->getFilterData($this);
+
+ if (null === $data) {
+ $error = 'Unable to apply filter without filter data.';
+ $error .= ' Either ensure the related data table has filter data or pass one explicitly.';
+
+ throw new BadMethodCallException($error);
+ }
+
+ $this->config->getType()->apply($query, $data, $this, $this->config->getOptions());
}
public function createView(FilterData $data, DataTableView $parent): FilterView
{
- $view = $this->type->createView($this, $data, $parent);
+ $view = $this->config->getType()->createView($this, $data, $parent);
- $this->type->buildView($view, $this, $data, $this->options);
+ $this->config->getType()->buildView($view, $this, $data, $this->config->getOptions());
return $view;
}
diff --git a/src/Filter/FilterBuilder.php b/src/Filter/FilterBuilder.php
new file mode 100755
index 00000000..b728fd36
--- /dev/null
+++ b/src/Filter/FilterBuilder.php
@@ -0,0 +1,24 @@
+locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ return new Filter($this->getFilterConfig());
+ }
+
+ private function createBuilderLockedException(): BadMethodCallException
+ {
+ return new BadMethodCallException('FilterBuilder methods cannot be accessed anymore once the builder is turned into a FilterConfigInterface instance.');
+ }
+}
diff --git a/src/Filter/FilterBuilderInterface.php b/src/Filter/FilterBuilderInterface.php
new file mode 100755
index 00000000..022c7993
--- /dev/null
+++ b/src/Filter/FilterBuilderInterface.php
@@ -0,0 +1,10 @@
+name;
+ }
+
+ public function setName(string $name): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->name = $name;
+
+ return $this;
+ }
+
+ public function getType(): ResolvedFilterTypeInterface
+ {
+ return $this->type;
+ }
+
+ public function setType(ResolvedFilterTypeInterface $type): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->type = $type;
+
+ return $this;
+ }
+
+ public function getOptions(): array
+ {
+ return $this->options;
+ }
+
+ public function hasOption(string $name): bool
+ {
+ return array_key_exists($name, $this->options);
+ }
+
+ public function getOption(string $name, mixed $default = null): mixed
+ {
+ return $this->options[$name] ?? $default;
+ }
+
+ public function setOptions(array $options): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->options = $options;
+
+ return $this;
+ }
+
+ public function setOption(string $name, mixed $value): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->options[$name] = $value;
+
+ return $this;
+ }
+
+ public function getFormType(): string
+ {
+ return $this->formType;
+ }
+
+ public function setFormType(string $formType): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->formType = $formType;
+
+ return $this;
+ }
+
+ public function getFormOptions(): array
+ {
+ return $this->formOptions;
+ }
+
+ public function setFormOptions(array $formOptions): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->formOptions = $formOptions;
+
+ return $this;
+ }
+
+ public function getOperatorFormType(): string
+ {
+ return $this->operatorFormType;
+ }
+
+ public function setOperatorFormType(string $operatorFormType): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->operatorFormType = $operatorFormType;
+
+ return $this;
+ }
+
+ public function getOperatorFormOptions(): array
+ {
+ return $this->operatorFormOptions;
+ }
+
+ public function setOperatorFormOptions(array $operatorFormOptions): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->operatorFormOptions = $operatorFormOptions;
+
+ return $this;
+ }
+
+ public function getSupportedOperators(): array
+ {
+ return array_unique([...$this->supportedOperators, $this->defaultOperator], SORT_REGULAR);
+ }
+
+ public function setSupportedOperators(array $supportedOperators): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->supportedOperators = $supportedOperators;
+
+ return $this;
+ }
+
+ public function getDefaultOperator(): Operator
+ {
+ // TODO: Remove "getNonDeprecatedCase()" call once the deprecated operators are removed.
+ return $this->defaultOperator->getNonDeprecatedCase();
+ }
+
+ public function setDefaultOperator(Operator $defaultOperator): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->defaultOperator = $defaultOperator;
+
+ return $this;
+ }
+
+ public function isOperatorSelectable(): bool
+ {
+ return $this->operatorSelectable;
+ }
+
+ public function setOperatorSelectable(bool $operatorSelectable): static
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $this->operatorSelectable = $operatorSelectable;
+
+ return $this;
+ }
+
+ public function getFilterConfig(): FilterConfigInterface
+ {
+ if ($this->locked) {
+ throw $this->createBuilderLockedException();
+ }
+
+ $config = clone $this;
+ $config->locked = true;
+
+ return $config;
+ }
+
+ private function createBuilderLockedException(): BadMethodCallException
+ {
+ return new BadMethodCallException('FilterConfigBuilder methods cannot be accessed anymore once the builder is turned into a FilterConfigInterface instance.');
+ }
+}
diff --git a/src/Filter/FilterConfigBuilderInterface.php b/src/Filter/FilterConfigBuilderInterface.php
new file mode 100755
index 00000000..12827adf
--- /dev/null
+++ b/src/Filter/FilterConfigBuilderInterface.php
@@ -0,0 +1,53 @@
+ $formType
+ */
+ public function setFormType(string $formType): static;
+
+ public function setFormOptions(array $formOptions): static;
+
+ /**
+ * @param class-string $operatorFormType
+ */
+ public function setOperatorFormType(string $operatorFormType): static;
+
+ public function setOperatorFormOptions(array $operatorFormOptions): static;
+
+ /**
+ * @param array $supportedOperators
+ */
+ public function setSupportedOperators(array $supportedOperators): static;
+
+ public function setDefaultOperator(Operator $defaultOperator): static;
+
+ public function setOperatorSelectable(bool $operatorSelectable): static;
+
+ public function getFilterConfig(): FilterConfigInterface;
+}
diff --git a/src/Filter/FilterConfigInterface.php b/src/Filter/FilterConfigInterface.php
new file mode 100755
index 00000000..c74f983e
--- /dev/null
+++ b/src/Filter/FilterConfigInterface.php
@@ -0,0 +1,29 @@
+
+ */
+ public function getSupportedOperators(): array;
+
+ public function getDefaultOperator(): Operator;
+
+ public function isOperatorSelectable(): bool;
+}
diff --git a/src/Filter/FilterData.php b/src/Filter/FilterData.php
old mode 100644
new mode 100755
index bc9985d5..f3e2ed1a
--- a/src/Filter/FilterData.php
+++ b/src/Filter/FilterData.php
@@ -11,7 +11,7 @@ class FilterData
{
public function __construct(
private mixed $value = '',
- private mixed $operator = null,
+ private ?Operator $operator = null,
) {
}
@@ -24,12 +24,11 @@ public static function fromArray(array $data = []): self
])
->setAllowedTypes('operator', ['null', 'string', Operator::class])
->setNormalizer('operator', function (Options $options, mixed $value): ?Operator {
- return is_string($value) ? Operator::from($value) : $value;
+ // TODO: Remove call to "getNonDeprecatedCase()"
+ return is_string($value) ? Operator::from($value)->getNonDeprecatedCase() : $value;
})
;
- $data = array_intersect_key($data, array_flip($resolver->getDefinedOptions()));
-
$data = $resolver->resolve($data);
return new self(
diff --git a/src/Filter/FilterFactory.php b/src/Filter/FilterFactory.php
old mode 100644
new mode 100755
index efaf9808..6061fc55
--- a/src/Filter/FilterFactory.php
+++ b/src/Filter/FilterFactory.php
@@ -4,24 +4,38 @@
namespace Kreyu\Bundle\DataTableBundle\Filter;
-use Kreyu\Bundle\DataTableBundle\Filter\Type\FilterTypeInterface;
+use Kreyu\Bundle\DataTableBundle\Filter\Type\FilterType;
class FilterFactory implements FilterFactoryInterface
{
public function __construct(
- private FilterRegistryInterface $registry,
+ private readonly FilterRegistryInterface $registry,
) {
}
- /**
- * @param class-string $type
- */
- public function create(string $name, string $type, array $options = []): FilterInterface
+ public function create(string $type = FilterType::class, array $options = []): FilterInterface
+ {
+ return $this->createBuilder($type, $options)->getFilter();
+ }
+
+ public function createNamed(string $name, string $type = FilterType::class, array $options = []): FilterInterface
+ {
+ return $this->createNamedBuilder($name, $type, $options)->getFilter();
+ }
+
+ public function createBuilder(string $type = FilterType::class, array $options = []): FilterBuilderInterface
+ {
+ return $this->createNamedBuilder($this->registry->getType($type)->getBlockPrefix(), $type, $options);
+ }
+
+ public function createNamedBuilder(string $name, string $type = FilterType::class, array $options = []): FilterBuilderInterface
{
$type = $this->registry->getType($type);
- $optionsResolver = $type->getOptionsResolver();
+ $builder = $type->createBuilder($this, $name, $options);
+
+ $type->buildFilter($builder, $builder->getOptions());
- return new Filter($name, $type, $optionsResolver->resolve($options));
+ return $builder;
}
}
diff --git a/src/Filter/FilterFactoryBuilder.php b/src/Filter/FilterFactoryBuilder.php
new file mode 100644
index 00000000..0c5078e0
--- /dev/null
+++ b/src/Filter/FilterFactoryBuilder.php
@@ -0,0 +1,88 @@
+resolvedTypeFactory = $resolvedTypeFactory;
+
+ return $this;
+ }
+
+ public function addExtension(FilterExtensionInterface $extension): static
+ {
+ $this->extensions[] = $extension;
+
+ return $this;
+ }
+
+ public function addExtensions(array $extensions): static
+ {
+ $this->extensions = array_merge($this->extensions, $extensions);
+
+ return $this;
+ }
+
+ public function addType(FilterTypeInterface $type): static
+ {
+ $this->types[] = $type;
+
+ return $this;
+ }
+
+ public function addTypes(array $types): static
+ {
+ foreach ($types as $type) {
+ $this->types[] = $type;
+ }
+
+ return $this;
+ }
+
+ public function addTypeExtension(FilterTypeExtensionInterface $typeExtension): static
+ {
+ foreach ($typeExtension::getExtendedTypes() as $extendedType) {
+ $this->typeExtensions[$extendedType][] = $typeExtension;
+ }
+
+ return $this;
+ }
+
+ public function addTypeExtensions(array $typeExtensions): static
+ {
+ foreach ($typeExtensions as $typeExtension) {
+ $this->addTypeExtension($typeExtension);
+ }
+
+ return $this;
+ }
+
+ public function getFilterFactory(): FilterFactoryInterface
+ {
+ $extensions = $this->extensions;
+
+ if (\count($this->types) > 0 || \count($this->typeExtensions) > 0) {
+ $extensions[] = new PreloadedFilterExtension($this->types, $this->typeExtensions);
+ }
+
+ $registry = new FilterRegistry($extensions, $this->resolvedTypeFactory ?? new ResolvedFilterTypeFactory());
+
+ return new FilterFactory($registry);
+ }
+}
diff --git a/src/Filter/FilterFactoryBuilderInterface.php b/src/Filter/FilterFactoryBuilderInterface.php
new file mode 100644
index 00000000..fe717be2
--- /dev/null
+++ b/src/Filter/FilterFactoryBuilderInterface.php
@@ -0,0 +1,38 @@
+ $extensions
+ */
+ public function addExtensions(array $extensions): static;
+
+ public function addType(FilterTypeInterface $type): static;
+
+ /**
+ * @param array $types
+ */
+ public function addTypes(array $types): static;
+
+ public function addTypeExtension(FilterTypeExtensionInterface $typeExtension): static;
+
+ /**
+ * @param array $typeExtensions
+ */
+ public function addTypeExtensions(array $typeExtensions): static;
+
+ public function getFilterFactory(): FilterFactoryInterface;
+}
diff --git a/src/Filter/FilterFactoryInterface.php b/src/Filter/FilterFactoryInterface.php
old mode 100644
new mode 100755
index a135abd3..2ea846d8
--- a/src/Filter/FilterFactoryInterface.php
+++ b/src/Filter/FilterFactoryInterface.php
@@ -4,12 +4,37 @@
namespace Kreyu\Bundle\DataTableBundle\Filter;
+use Kreyu\Bundle\DataTableBundle\Filter\Type\FilterType;
use Kreyu\Bundle\DataTableBundle\Filter\Type\FilterTypeInterface;
+use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
interface FilterFactoryInterface
{
/**
* @param class-string $type
+ *
+ * @throws InvalidOptionsException if any of given option is not applicable to the given type
*/
- public function create(string $name, string $type, array $options = []): FilterInterface;
+ public function create(string $type = FilterType::class, array $options = []): FilterInterface;
+
+ /**
+ * @param class-string $type
+ *
+ * @throws InvalidOptionsException if any of given option is not applicable to the given type
+ */
+ public function createNamed(string $name, string $type = FilterType::class, array $options = []): FilterInterface;
+
+ /**
+ * @param class-string $type
+ *
+ * @throws InvalidOptionsException if any of given option is not applicable to the given type
+ */
+ public function createBuilder(string $type = FilterType::class, array $options = []): FilterBuilderInterface;
+
+ /**
+ * @param class-string $type
+ *
+ * @throws InvalidOptionsException if any of given option is not applicable to the given type
+ */
+ public function createNamedBuilder(string $name, string $type = FilterType::class, array $options = []): FilterBuilderInterface;
}
diff --git a/src/Filter/FilterInterface.php b/src/Filter/FilterInterface.php
old mode 100644
new mode 100755
index bfac5a58..82b6a5aa
--- a/src/Filter/FilterInterface.php
+++ b/src/Filter/FilterInterface.php
@@ -4,27 +4,31 @@
namespace Kreyu\Bundle\DataTableBundle\Filter;
+use Kreyu\Bundle\DataTableBundle\DataTableInterface;
use Kreyu\Bundle\DataTableBundle\DataTableView;
-use Kreyu\Bundle\DataTableBundle\Filter\Type\ResolvedFilterTypeInterface;
use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
interface FilterInterface
{
- public function apply(ProxyQueryInterface $query, FilterData $data): void;
-
public function getName(): string;
+ public function getConfig(): FilterConfigInterface;
+
+ public function getDataTable(): DataTableInterface;
+
+ public function setDataTable(DataTableInterface $dataTable): static;
+
public function getFormName(): string;
public function getFormOptions(): array;
public function getQueryPath(): string;
- public function getType(): ResolvedFilterTypeInterface;
-
- public function getOptions(): array;
-
- public function getOption(string $name): mixed;
+ /**
+ * @param ProxyQueryInterface|null $query if not given, filter will be applied to the related data table query
+ * @param FilterData|null $data if not given, filter will be applied using filter data from the related data table
+ */
+ public function apply(ProxyQueryInterface $query = null, FilterData $data = null): void;
public function createView(FilterData $data, DataTableView $parent): FilterView;
}
diff --git a/src/Filter/FilterRegistry.php b/src/Filter/FilterRegistry.php
old mode 100644
new mode 100755
index b19abd69..69624d38
--- a/src/Filter/FilterRegistry.php
+++ b/src/Filter/FilterRegistry.php
@@ -4,110 +4,33 @@
namespace Kreyu\Bundle\DataTableBundle\Filter;
-use Kreyu\Bundle\DataTableBundle\Exception\UnexpectedTypeException;
-use Kreyu\Bundle\DataTableBundle\Filter\Extension\FilterTypeExtensionInterface;
+use Kreyu\Bundle\DataTableBundle\AbstractRegistry;
+use Kreyu\Bundle\DataTableBundle\Filter\Extension\FilterExtensionInterface;
use Kreyu\Bundle\DataTableBundle\Filter\Type\FilterTypeInterface;
-use Kreyu\Bundle\DataTableBundle\Filter\Type\ResolvedFilterTypeFactoryInterface;
use Kreyu\Bundle\DataTableBundle\Filter\Type\ResolvedFilterTypeInterface;
-class FilterRegistry implements FilterRegistryInterface
+/**
+ * @extends AbstractRegistry
+ */
+class FilterRegistry extends AbstractRegistry implements FilterRegistryInterface
{
- /**
- * @var array
- */
- private array $types = [];
-
- /**
- * @var array
- */
- private array $resolvedTypes = [];
-
- /**
- * @var array
- */
- private array $checkedTypes = [];
-
- /**
- * @var array
- */
- private array $typeExtensions = [];
-
- /**
- * @param iterable $types
- * @param iterable $typeExtensions
- */
- public function __construct(
- iterable $types,
- iterable $typeExtensions,
- private ResolvedFilterTypeFactoryInterface $resolvedFilterTypeFactory,
- ) {
- foreach ($types as $type) {
- if (!$type instanceof FilterTypeInterface) {
- throw new UnexpectedTypeException($type, FilterTypeInterface::class);
- }
-
- $this->types[$type::class] = $type;
- }
-
- foreach ($typeExtensions as $typeExtension) {
- if (!$typeExtension instanceof FilterTypeExtensionInterface) {
- throw new UnexpectedTypeException($typeExtension, FilterTypeExtensionInterface::class);
- }
-
- $this->typeExtensions[$typeExtension::class] = $typeExtension;
- }
- }
-
public function getType(string $name): ResolvedFilterTypeInterface
{
- if (!isset($this->resolvedTypes[$name])) {
- if (!isset($this->types[$name])) {
- throw new \InvalidArgumentException(sprintf('Could not load type "%s".', $name));
- }
-
- $this->resolvedTypes[$name] = $this->resolveType($this->types[$name]);
- }
-
- return $this->resolvedTypes[$name];
+ return $this->doGetType($name);
}
- private function resolveType(FilterTypeInterface $type): ResolvedFilterTypeInterface
+ final protected function getErrorContextName(): string
{
- $fqcn = $type::class;
-
- if (isset($this->checkedTypes[$fqcn])) {
- $types = implode(' > ', array_merge(array_keys($this->checkedTypes), [$fqcn]));
- throw new \LogicException(sprintf('Circular reference detected for filter type "%s" (%s).', $fqcn, $types));
- }
-
- $this->checkedTypes[$fqcn] = true;
-
- $typeExtensions = array_filter(
- $this->typeExtensions,
- fn (FilterTypeExtensionInterface $extension) => $this->isFqcnExtensionEligible($fqcn, $extension),
- );
-
- $parentType = $type->getParent();
-
- try {
- return $this->resolvedFilterTypeFactory->createResolvedType(
- $type,
- $typeExtensions,
- $parentType ? $this->getType($parentType) : null,
- );
- } finally {
- unset($this->checkedTypes[$fqcn]);
- }
+ return 'filter';
}
- private function isFqcnExtensionEligible(string $fqcn, FilterTypeExtensionInterface $extension): bool
+ final protected function getTypeClass(): string
{
- $extendedTypes = $extension::getExtendedTypes();
-
- if ($extendedTypes instanceof \Traversable) {
- $extendedTypes = iterator_to_array($extendedTypes);
- }
+ return FilterTypeInterface::class;
+ }
- return in_array($fqcn, $extendedTypes);
+ final protected function getExtensionClass(): string
+ {
+ return FilterExtensionInterface::class;
}
}
diff --git a/src/Filter/FilterRegistryInterface.php b/src/Filter/FilterRegistryInterface.php
old mode 100644
new mode 100755
diff --git a/src/Filter/FilterView.php b/src/Filter/FilterView.php
old mode 100644
new mode 100755
diff --git a/src/Filter/FiltrationData.php b/src/Filter/FiltrationData.php
old mode 100644
new mode 100755
index cbc817ef..dc77bf8d
--- a/src/Filter/FiltrationData.php
+++ b/src/Filter/FiltrationData.php
@@ -57,8 +57,8 @@ public static function fromDataTable(DataTableInterface $dataTable): self
{
$filters = [];
- foreach ($dataTable->getConfig()->getFilters() as $filter) {
- $filters[$filter->getName()] = new FilterData();
+ foreach ($dataTable->getFilters() as $filter) {
+ $filters[$filter->getName()] = new FilterData(operator: $filter->getConfig()->getDefaultOperator());
}
return new self($filters);
@@ -121,9 +121,9 @@ public function removeFilter(string|FilterInterface $filter): void
*/
public function appendMissingFilters(array $filters, FilterData $data = new FilterData()): void
{
- foreach ($filters as $column) {
- if (null === $this->getFilterData($column)) {
- $this->setFilterData($column, $data);
+ foreach ($filters as $filter) {
+ if (null === $this->getFilterData($filter)) {
+ $this->setFilterData($filter, $data);
}
}
}
diff --git a/src/Filter/Form/Type/DateRangeType.php b/src/Filter/Form/Type/DateRangeType.php
old mode 100644
new mode 100755
index 4dc4e5a7..33e2d72e
--- a/src/Filter/Form/Type/DateRangeType.php
+++ b/src/Filter/Form/Type/DateRangeType.php
@@ -16,12 +16,14 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
$builder
->add('from', DateType::class, [
'widget' => 'single_text',
+ 'label' => false,
'attr' => [
'autocomplete' => 'off',
],
])
->add('to', DateType::class, [
'widget' => 'single_text',
+ 'label' => false,
'attr' => [
'autocomplete' => 'off',
],
@@ -55,6 +57,8 @@ public function mapFormsToData(\Traversable $forms, mixed &$viewData): void
$to = $forms['to']->getData();
if (null === $from && null === $to) {
+ $viewData = '';
+
return;
}
diff --git a/src/Filter/Form/Type/FilterDataType.php b/src/Filter/Form/Type/FilterDataType.php
old mode 100644
new mode 100755
index 9df97f7a..efc626ad
--- a/src/Filter/Form/Type/FilterDataType.php
+++ b/src/Filter/Form/Type/FilterDataType.php
@@ -7,62 +7,87 @@
use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
use Kreyu\Bundle\DataTableBundle\Filter\Operator;
use Symfony\Component\Form\AbstractType;
-use Symfony\Component\Form\CallbackTransformer;
+use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
-class FilterDataType extends AbstractType
+class FilterDataType extends AbstractType implements DataMapperInterface
{
+ public function buildView(FormView $view, FormInterface $form, array $options): void
+ {
+ $view->vars['operator_selectable'] = $options['operator_selectable'];
+ }
+
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
- ->add('operator', $options['operator_type'], $options['operator_options'] + [
- 'label' => false,
- 'required' => false,
-// 'getter' => fn (FilterData $data) => $data->getOperator(),
-// 'setter' => fn (FilterData $data, Operator $operator) => $data->setOperator($operator),
- ])
- ->add('value', $options['field_type'], $options['field_options'] + [
- 'label' => false,
+ ->add('value', $options['form_type'], $options['form_options'])
+ ->add('operator', $options['operator_form_type'], $options['operator_form_options'])
+ ->setDataMapper($this)
+ ;
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver
+ ->setDefaults([
'required' => false,
- 'empty_data' => '',
+ 'data_class' => FilterData::class,
+ 'form_type' => TextType::class,
+ 'form_options' => [],
+ 'operator_form_type' => OperatorType::class,
+ 'operator_form_options' => [],
+ 'default_operator' => Operator::Equals,
+ 'supported_operators' => [],
+ 'operator_selectable' => false,
])
+ ->setAllowedTypes('form_type', 'string')
+ ->setAllowedTypes('form_options', 'array')
+ ->setAllowedTypes('operator_form_type', 'string')
+ ->setAllowedTypes('operator_form_options', 'array')
+ ->setAllowedTypes('operator_selectable', 'bool')
+ ->setAllowedTypes('default_operator', Operator::class)
+ ->setAllowedTypes('supported_operators', Operator::class.'[]')
;
-
- $builder->get('value')->addModelTransformer(new CallbackTransformer(
- fn (mixed $value) => $value,
- fn (mixed $value) => $value ?? '',
- ));
-
- $builder->get('operator')->addViewTransformer(new CallbackTransformer(
- fn (mixed $value) => $value,
- fn (mixed $value) => $value instanceof Operator ? $value->value : $value,
- ));
}
- public function configureOptions(OptionsResolver $resolver): void
+ public function getBlockPrefix(): string
{
- $resolver->setDefaults([
- 'required' => false,
- 'data_class' => FilterData::class,
- 'operator_type' => OperatorType::class,
- 'operator_options' => [
- 'visible' => false,
- ],
- 'field_type' => TextType::class,
- 'field_options' => [],
- ]);
+ return 'kreyu_data_table_filter_data';
+ }
- $resolver->setAllowedTypes('operator_type', 'string');
- $resolver->setAllowedTypes('operator_options', 'array');
+ public function mapDataToForms(mixed $viewData, \Traversable $forms): void
+ {
+ if (!$viewData instanceof FilterData) {
+ return;
+ }
- $resolver->setAllowedTypes('field_type', 'string');
- $resolver->setAllowedTypes('field_options', 'array');
+ $forms = iterator_to_array($forms);
+ $forms['value']->setData($viewData->hasValue() ? $viewData->getValue() : null);
+ $forms['operator']->setData($viewData->getOperator());
}
- public function getBlockPrefix(): string
+ public function mapFormsToData(\Traversable $forms, mixed &$viewData): void
{
- return 'kreyu_data_table_filter_data';
+ if (!$viewData instanceof FilterData) {
+ $viewData = new FilterData();
+ }
+
+ $forms = iterator_to_array($forms);
+
+ $operator = $forms['operator']->getData();
+
+ if (is_string($operator)) {
+ $operator = Operator::tryFrom($operator);
+ }
+
+ // TODO: Remove once the deprecated operators are removed.
+ $operator = $operator?->getNonDeprecatedCase();
+
+ $viewData->setValue($forms['value']->getData() ?? '');
+ $viewData->setOperator($operator);
}
}
diff --git a/src/Filter/Form/Type/FiltrationDataType.php b/src/Filter/Form/Type/FiltrationDataType.php
old mode 100644
new mode 100755
index c096e544..65660d86
--- a/src/Filter/Form/Type/FiltrationDataType.php
+++ b/src/Filter/Form/Type/FiltrationDataType.php
@@ -26,15 +26,15 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
*/
$dataTable = $options['data_table'];
- foreach ($dataTable->getConfig()->getFilters() as $filter) {
+ foreach ($dataTable->getFilters() as $filter) {
$builder->add($filter->getFormName(), FilterDataType::class, array_merge($filter->getFormOptions() + [
+ 'empty_data' => new FilterData(),
'getter' => function (FiltrationData $filtrationData, FormInterface $form) {
return $filtrationData->getFilterData($form->getName());
},
'setter' => function (FiltrationData $filtrationData, FilterData $filterData, FormInterface $form) {
$filtrationData->setFilterData($form->getName(), $filterData);
},
- 'empty_data' => new FilterData(),
]));
}
@@ -54,29 +54,27 @@ public function finishView(FormView $view, FormInterface $form, array $options):
throw new \LogicException('Unable to create filtration form view without the data table view.');
}
- $view->vars['attr']['id'] = $id = $view->vars['id'];
+ $this->applyFormAttributeRecursively($view, $id = $view->vars['id']);
- $this->applyFormAttributeRecursively($view, $id);
+ $view->vars['attr']['id'] = $id;
foreach ($view as $name => $filterFormView) {
$filterView = $dataTableView->filters[$name];
- $filterFormView->vars = array_replace($filterFormView->vars, [
- 'label' => $filterView->vars['label'],
- 'translation_domain' => $filterView->vars['translation_domain'],
- ]);
+ $filterFormView->vars['label'] = $filterView->vars['label'];
+ $filterFormView->vars['translation_domain'] = $filterView->vars['translation_domain'];
}
$searchFields = [];
foreach ($form as $child) {
try {
- $filter = $dataTable->getConfig()->getFilter($child->getName());
+ $filter = $dataTable->getFilter($child->getName());
} catch (\InvalidArgumentException) {
continue;
}
- if ($filter->getType()->getInnerType() instanceof SearchFilterTypeInterface) {
+ if ($filter->getConfig()->getType()->getInnerType() instanceof SearchFilterTypeInterface) {
$searchField = $view[$child->getName()];
$searchField->vars['attr']['form'] = $view->vars['id'];
@@ -112,14 +110,8 @@ public function mapDataToForms(mixed $viewData, \Traversable $forms): void
return;
}
- $forms = iterator_to_array($forms);
-
- foreach ($forms as $name => $form) {
- $filterData = $viewData->getFilterData($name);
-
- if ($filterData && $filterData->hasValue()) {
- $form->setData($filterData);
- }
+ foreach (iterator_to_array($forms) as $name => $form) {
+ $form->setData($viewData->getFilterData($name));
}
}
@@ -129,9 +121,7 @@ public function mapFormsToData(\Traversable $forms, mixed &$viewData): void
$viewData = new FiltrationData();
}
- $forms = iterator_to_array($forms);
-
- foreach ($forms as $name => $form) {
+ foreach (iterator_to_array($forms) as $name => $form) {
$viewData->setFilterData($name, $form->getData());
}
}
diff --git a/src/Filter/Form/Type/OperatorType.php b/src/Filter/Form/Type/OperatorType.php
old mode 100644
new mode 100755
index dd5cb69f..8a67675f
--- a/src/Filter/Form/Type/OperatorType.php
+++ b/src/Filter/Form/Type/OperatorType.php
@@ -21,10 +21,16 @@ public function finishView(FormView $view, FormInterface $form, array $options):
public function configureOptions(OptionsResolver $resolver): void
{
$resolver
- ->setDefault('class', Operator::class)
- ->setDefault('placeholder', false)
- ->setDefault('visible', false)
- ->setDefault('choice_translation_domain', 'KreyuDataTable')
+ ->setDefaults([
+ 'class' => Operator::class,
+ 'choice_translation_domain' => 'KreyuDataTable',
+ 'choice_label' => 'label',
+ 'label' => false,
+ 'placeholder' => false,
+ 'visible' => false,
+ 'required' => false,
+ ])
+ ->setAllowedTypes('visible', 'bool')
;
}
diff --git a/src/Filter/Operator.php b/src/Filter/Operator.php
old mode 100644
new mode 100755
index 884d009b..0b4bacff
--- a/src/Filter/Operator.php
+++ b/src/Filter/Operator.php
@@ -6,14 +6,103 @@
enum Operator: string
{
- case EQUALS = 'equals';
- case CONTAINS = 'contains';
- case NOT_CONTAINS = 'not-contains';
- case NOT_EQUALS = 'not-equals';
- case GREATER_THAN = 'greater-than';
- case GREATER_THAN_EQUALS = 'greater-than-equals';
- case LESS_THAN_EQUALS = 'less-than-equals';
- case LESS_THAN = 'less-than';
- case STARTS_WITH = 'starts-with';
- case ENDS_WITH = 'ends-with';
+ case Equals = 'equals';
+ case NotEquals = 'not-equals';
+ case Contains = 'contains';
+ case NotContains = 'not-contains';
+ case GreaterThan = 'greater-than';
+ case GreaterThanEquals = 'greater-than-equals';
+ case LessThan = 'less-than';
+ case LessThanEquals = 'less-than-equals';
+ case StartsWith = 'starts-with';
+ case EndsWith = 'ends-with';
+
+ // TODO: Remove deprecated cases
+
+ /**
+ * @deprecated since 0.14.0, use {@see Operator::Equals} instead
+ */
+ case EQUALS = 'deprecated-equals';
+
+ /**
+ * @deprecated since 0.14.0, use {@see Operator::NotEquals} instead
+ */
+ case CONTAINS = 'deprecated-contains';
+
+ /**
+ * @deprecated since 0.14.0, use {@see Operator::Contains} instead
+ */
+ case NOT_CONTAINS = 'deprecated-not-contains';
+
+ /**
+ * @deprecated since 0.14.0, use {@see Operator::NotContains} instead
+ */
+ case NOT_EQUALS = 'deprecated-not-equals';
+
+ /**
+ * @deprecated since 0.14.0, use {@see Operator::GreaterThan} instead
+ */
+ case GREATER_THAN = 'deprecated-greater-than';
+
+ /**
+ * @deprecated since 0.14.0, use {@see Operator::GreaterThanEquals} instead
+ */
+ case GREATER_THAN_EQUALS = 'deprecated-greater-than-equals';
+
+ /**
+ * @deprecated since 0.14.0, use {@see Operator::LessThan} instead
+ */
+ case LESS_THAN = 'deprecated-less-than';
+
+ /**
+ * @deprecated since 0.14.0, use {@see Operator::LessThanEquals} instead
+ */
+ case LESS_THAN_EQUALS = 'deprecated-less-than-equals';
+
+ /**
+ * @deprecated since 0.14.0, use {@see Operator::StartsWith} instead
+ */
+ case STARTS_WITH = 'deprecated-starts-with';
+
+ /**
+ * @deprecated since 0.14.0, use {@see Operator::EndsWith} instead
+ */
+ case ENDS_WITH = 'deprecated-ends-with';
+
+ public function getLabel(): string
+ {
+ // TODO: Remove deprecated cases labels
+ return match ($this) {
+ self::EQUALS, self::Equals => 'Equals',
+ self::NOT_CONTAINS, self::NotContains => 'Not contains',
+ self::CONTAINS, self::Contains => 'Contains',
+ self::NOT_EQUALS, self::NotEquals => 'Not equals',
+ self::GREATER_THAN, self::GreaterThan => 'Greater than',
+ self::GREATER_THAN_EQUALS, self::GreaterThanEquals => 'Greater than or equal',
+ self::LESS_THAN, self::LessThan => 'Less than',
+ self::LESS_THAN_EQUALS, self::LessThanEquals => 'Less than or equal',
+ self::STARTS_WITH, self::StartsWith => 'Starts with',
+ self::ENDS_WITH, self::EndsWith => 'Ends with',
+ };
+ }
+
+ /**
+ * TODO: Remove this method after removing deprecated cases.
+ */
+ public function getNonDeprecatedCase(): self
+ {
+ return match ($this) {
+ self::EQUALS => self::Equals,
+ self::NOT_CONTAINS => self::NotContains,
+ self::CONTAINS => self::Contains,
+ self::NOT_EQUALS => self::NotEquals,
+ self::GREATER_THAN => self::GreaterThan,
+ self::GREATER_THAN_EQUALS => self::GreaterThanEquals,
+ self::LESS_THAN => self::LessThan,
+ self::LESS_THAN_EQUALS => self::LessThanEquals,
+ self::STARTS_WITH => self::StartsWith,
+ self::ENDS_WITH => self::EndsWith,
+ default => $this,
+ };
+ }
}
diff --git a/src/Filter/Type/AbstractFilterType.php b/src/Filter/Type/AbstractFilterType.php
old mode 100644
new mode 100755
index 7e57c1c7..380cdbaa
--- a/src/Filter/Type/AbstractFilterType.php
+++ b/src/Filter/Type/AbstractFilterType.php
@@ -4,13 +4,19 @@
namespace Kreyu\Bundle\DataTableBundle\Filter\Type;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FilterView;
+use Kreyu\Bundle\DataTableBundle\Util\StringUtil;
use Symfony\Component\OptionsResolver\OptionsResolver;
abstract class AbstractFilterType implements FilterTypeInterface
{
+ public function buildFilter(FilterBuilderInterface $builder, array $options): void
+ {
+ }
+
public function buildView(FilterView $view, FilterInterface $filter, FilterData $data, array $options): void
{
}
@@ -19,6 +25,11 @@ public function configureOptions(OptionsResolver $resolver): void
{
}
+ public function getBlockPrefix(): string
+ {
+ return StringUtil::fqcnToShortName(static::class, ['FilterType', 'Type']) ?: '';
+ }
+
public function getParent(): ?string
{
return FilterType::class;
diff --git a/src/Filter/Type/FilterType.php b/src/Filter/Type/FilterType.php
old mode 100644
new mode 100755
index 6a1d4404..d142f637
--- a/src/Filter/Type/FilterType.php
+++ b/src/Filter/Type/FilterType.php
@@ -4,15 +4,19 @@
namespace Kreyu\Bundle\DataTableBundle\Filter\Type;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FilterView;
use Kreyu\Bundle\DataTableBundle\Filter\Form\Type\OperatorType;
+use Kreyu\Bundle\DataTableBundle\Filter\Operator;
use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
use Kreyu\Bundle\DataTableBundle\Util\StringUtil;
+use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
+use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
-use Symfony\Component\Translation\TranslatableMessage;
+use Symfony\Contracts\Translation\TranslatableInterface;
final class FilterType implements FilterTypeInterface
{
@@ -20,6 +24,23 @@ public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterf
{
}
+ public function buildFilter(FilterBuilderInterface $builder, array $options): void
+ {
+ $setters = [
+ 'form_type' => $builder->setFormType(...),
+ 'form_options' => $builder->setFormOptions(...),
+ 'operator_form_type' => $builder->setOperatorFormType(...),
+ 'operator_form_options' => $builder->setOperatorFormOptions(...),
+ 'default_operator' => $builder->setDefaultOperator(...),
+ 'supported_operators' => $builder->setSupportedOperators(...),
+ 'operator_selectable' => $builder->setOperatorSelectable(...),
+ ];
+
+ foreach ($setters as $option => $setter) {
+ $setter($options[$option]);
+ }
+ }
+
public function buildView(FilterView $view, FilterInterface $filter, FilterData $data, array $options): void
{
$value = $data;
@@ -42,10 +63,14 @@ public function buildView(FilterView $view, FilterInterface $filter, FilterData
'field_options' => $options['field_options'],
'operator_type' => $options['operator_type'],
'operator_options' => $options['operator_options'],
- 'auto_alias_resolving' => $options['auto_alias_resolving'],
'active_filter_formatter' => $options['active_filter_formatter'],
'data' => $view->data,
'value' => $view->value,
+ 'operator_selectable' => $filter->getConfig()->isOperatorSelectable(),
+ 'clear_filter_parameters' => [
+ 'value' => $options['empty_data'],
+ 'operator' => $filter->getConfig()->getDefaultOperator()->value,
+ ],
]);
}
@@ -57,29 +82,101 @@ public function configureOptions(OptionsResolver $resolver): void
'label_translation_parameters' => [],
'translation_domain' => null,
'query_path' => null,
- 'field_type' => TextType::class,
+ 'form_type' => TextType::class,
+ 'form_options' => [],
+ 'operator_form_type' => OperatorType::class,
+ 'operator_form_options' => [],
+ 'default_operator' => Operator::Equals,
+ 'supported_operators' => [],
+ 'operator_selectable' => false,
+ 'active_filter_formatter' => function (FilterData $data): mixed {
+ return $data->getValue();
+ },
+ 'empty_data' => '',
+
+ // TODO: Remove deprecated options
+ 'auto_alias_resolving' => true,
+ 'field_type' => null,
'field_options' => [],
- 'operator_type' => OperatorType::class,
+ 'operator_type' => null,
'operator_options' => [
- 'visible' => false,
+ 'visible' => null,
'choices' => [],
],
- 'auto_alias_resolving' => true,
- 'active_filter_formatter' => function (FilterData $data): mixed {
- return $data->getValue();
- },
])
- ->setAllowedTypes('label', ['null', 'bool', 'string', TranslatableMessage::class])
+ ->addNormalizer('form_options', function (Options $options, array $value): array {
+ return $value + ['required' => false];
+ })
+ ->addNormalizer('operator_form_type', function (Options $options, string $value): string {
+ return $options['operator_selectable'] ? $value : HiddenType::class;
+ })
+ ->addNormalizer('operator_form_options', function (Options $options, array $value): array {
+ if (!$options['operator_selectable']) {
+ $value['data'] ??= $options['default_operator']->value;
+ }
+
+ if (is_a($options['operator_form_type'], OperatorType::class, true)) {
+ $value['choices'] ??= $options['supported_operators'];
+ $value['empty_data'] ??= $options['default_operator'];
+ }
+
+ return $value;
+ })
+ ->setAllowedTypes('label', ['null', 'bool', 'string', TranslatableInterface::class])
->setAllowedTypes('query_path', ['null', 'string'])
- ->setAllowedTypes('field_type', ['string'])
+ ->setAllowedTypes('form_type', ['string'])
+ ->setAllowedTypes('form_options', ['array'])
+ ->setAllowedTypes('operator_form_type', ['string'])
+ ->setAllowedTypes('operator_form_options', ['array'])
+ ->setAllowedTypes('active_filter_formatter', ['null', 'callable'])
+ ->setAllowedTypes('empty_data', ['string', 'array'])
+ ;
+
+ // TODO: Remove logic below, as it is associated with deprecated options (for backwards compatibility)
+ $resolver
+ ->setAllowedTypes('field_type', ['null', 'string'])
->setAllowedTypes('field_options', ['array'])
- ->setAllowedTypes('operator_type', ['string'])
+ ->setAllowedTypes('operator_type', ['null', 'string'])
->setAllowedTypes('operator_options', ['array'])
- ->setAllowedTypes('auto_alias_resolving', ['bool'])
- ->setAllowedTypes('active_filter_formatter', ['null', 'callable'])
+ ->setDeprecated('field_type', 'kreyu/data-table-bundle', '0.14', 'The "%name%" option is deprecated, use "form_type" instead.')
+ ->setDeprecated('field_options', 'kreyu/data-table-bundle', '0.14', 'The "%name%" option is deprecated, use "form_options" instead.')
+ ->setDeprecated('operator_type', 'kreyu/data-table-bundle', '0.14', 'The "%name%" option is deprecated, use "operator_form_type" instead.')
+ ->setDeprecated('operator_options', 'kreyu/data-table-bundle', '0.14', 'The "%name%" option is deprecated, use "operator_form_options", "supported_operators", "operator_selectable" and "default_operator" instead.')
+ ->addNormalizer('form_type', function (Options $options, mixed $value) {
+ return $options['field_type'] ?? $value;
+ })
+ ->addNormalizer('form_options', function (Options $options, mixed $value) {
+ return $options['field_options'] ?: $value;
+ })
+ ->addNormalizer('operator_form_type', function (Options $options, mixed $value) {
+ return $options['operator_type'] ?? $value;
+ })
+ ->addNormalizer('operator_form_options', function (Options $options, mixed $value) {
+ if ($deprecatedValue = $options['operator_options']) {
+ unset($deprecatedValue['visible'], $deprecatedValue['choices']);
+ }
+
+ return $deprecatedValue ?: $value;
+ })
+ ->addNormalizer('supported_operators', function (Options $options, mixed $value) {
+ return ($options['operator_options']['choices'] ?? []) ?: $value;
+ })
+ ->addNormalizer('default_operator', function (Options $options, mixed $value) {
+ $deprecatedChoices = $options['operator_options']['choices'] ?? [];
+
+ return reset($deprecatedChoices) ?: $value;
+ })
+ ->addNormalizer('operator_selectable', function (Options $options, mixed $value) {
+ return ($options['operator_options']['visible'] ?? null) ?: $value;
+ })
;
}
+ public function getBlockPrefix(): string
+ {
+ return 'filter';
+ }
+
public function getParent(): ?string
{
return null;
diff --git a/src/Filter/Type/FilterTypeInterface.php b/src/Filter/Type/FilterTypeInterface.php
old mode 100644
new mode 100755
index 51782bcc..9803007a
--- a/src/Filter/Type/FilterTypeInterface.php
+++ b/src/Filter/Type/FilterTypeInterface.php
@@ -4,26 +4,25 @@
namespace Kreyu\Bundle\DataTableBundle\Filter\Type;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FilterView;
use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
-/**
- * @template T of ProxyQueryInterface
- */
interface FilterTypeInterface
{
- /**
- * @param T $query
- */
public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter, array $options): void;
+ public function buildFilter(FilterBuilderInterface $builder, array $options): void;
+
public function buildView(FilterView $view, FilterInterface $filter, FilterData $data, array $options): void;
public function configureOptions(OptionsResolver $resolver): void;
+ public function getBlockPrefix(): string;
+
/**
* @return class-string|null
*/
diff --git a/src/Filter/Type/ResolvedFilterType.php b/src/Filter/Type/ResolvedFilterType.php
old mode 100644
new mode 100755
index 44876411..d2b99db0
--- a/src/Filter/Type/ResolvedFilterType.php
+++ b/src/Filter/Type/ResolvedFilterType.php
@@ -6,10 +6,14 @@
use Kreyu\Bundle\DataTableBundle\DataTableView;
use Kreyu\Bundle\DataTableBundle\Filter\Extension\FilterTypeExtensionInterface;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterBuilder;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterFactoryInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FilterView;
use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
+use Symfony\Component\OptionsResolver\Exception\ExceptionInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ResolvedFilterType implements ResolvedFilterTypeInterface
@@ -26,6 +30,11 @@ public function __construct(
) {
}
+ public function getBlockPrefix(): string
+ {
+ return $this->innerType->getBlockPrefix();
+ }
+
public function getParent(): ?ResolvedFilterTypeInterface
{
return $this->parent;
@@ -41,11 +50,36 @@ public function getTypeExtensions(): array
return $this->typeExtensions;
}
+ /**
+ * @throws ExceptionInterface
+ */
+ public function createBuilder(FilterFactoryInterface $factory, string $name, array $options): FilterBuilderInterface
+ {
+ try {
+ $options = $this->getOptionsResolver()->resolve($options);
+ } catch (ExceptionInterface $exception) {
+ throw new $exception(sprintf('An error has occurred resolving the options of the filter "%s": ', get_debug_type($this->getInnerType())).$exception->getMessage(), $exception->getCode(), $exception);
+ }
+
+ return new FilterBuilder($name, $this, $options);
+ }
+
public function createView(FilterInterface $filter, FilterData $data, DataTableView $parent): FilterView
{
return new FilterView($parent, $data);
}
+ public function buildFilter(FilterBuilderInterface $builder, array $options): void
+ {
+ $this->parent?->buildFilter($builder, $options);
+
+ $this->innerType->buildFilter($builder, $options);
+
+ foreach ($this->typeExtensions as $extension) {
+ $extension->buildFilter($builder, $options);
+ }
+ }
+
public function buildView(FilterView $view, FilterInterface $filter, FilterData $data, array $options): void
{
$this->parent?->buildView($view, $filter, $data, $options);
diff --git a/src/Filter/Type/ResolvedFilterTypeFactory.php b/src/Filter/Type/ResolvedFilterTypeFactory.php
old mode 100644
new mode 100755
index 55e60332..99badbc8
--- a/src/Filter/Type/ResolvedFilterTypeFactory.php
+++ b/src/Filter/Type/ResolvedFilterTypeFactory.php
@@ -6,7 +6,7 @@
class ResolvedFilterTypeFactory implements ResolvedFilterTypeFactoryInterface
{
- public function createResolvedType(FilterTypeInterface $type, array $typeExtensions, ResolvedFilterTypeInterface $parent = null): ResolvedFilterTypeInterface
+ public function createResolvedType(FilterTypeInterface $type, array $typeExtensions = [], ResolvedFilterTypeInterface $parent = null): ResolvedFilterTypeInterface
{
return new ResolvedFilterType($type, $typeExtensions, $parent);
}
diff --git a/src/Filter/Type/ResolvedFilterTypeFactoryInterface.php b/src/Filter/Type/ResolvedFilterTypeFactoryInterface.php
old mode 100644
new mode 100755
index c522b13c..d475f539
--- a/src/Filter/Type/ResolvedFilterTypeFactoryInterface.php
+++ b/src/Filter/Type/ResolvedFilterTypeFactoryInterface.php
@@ -4,7 +4,12 @@
namespace Kreyu\Bundle\DataTableBundle\Filter\Type;
+use Kreyu\Bundle\DataTableBundle\Filter\Extension\FilterTypeExtensionInterface;
+
interface ResolvedFilterTypeFactoryInterface
{
- public function createResolvedType(FilterTypeInterface $type, array $typeExtensions, ResolvedFilterTypeInterface $parent = null): ResolvedFilterTypeInterface;
+ /**
+ * @param array $typeExtensions
+ */
+ public function createResolvedType(FilterTypeInterface $type, array $typeExtensions = [], ResolvedFilterTypeInterface $parent = null): ResolvedFilterTypeInterface;
}
diff --git a/src/Filter/Type/ResolvedFilterTypeInterface.php b/src/Filter/Type/ResolvedFilterTypeInterface.php
old mode 100644
new mode 100755
index 8ff78495..69e8b26a
--- a/src/Filter/Type/ResolvedFilterTypeInterface.php
+++ b/src/Filter/Type/ResolvedFilterTypeInterface.php
@@ -6,7 +6,9 @@
use Kreyu\Bundle\DataTableBundle\DataTableView;
use Kreyu\Bundle\DataTableBundle\Filter\Extension\FilterTypeExtensionInterface;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
+use Kreyu\Bundle\DataTableBundle\Filter\FilterFactoryInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
use Kreyu\Bundle\DataTableBundle\Filter\FilterView;
use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
@@ -14,6 +16,8 @@
interface ResolvedFilterTypeInterface
{
+ public function getBlockPrefix(): string;
+
public function getParent(): ?ResolvedFilterTypeInterface;
public function getInnerType(): FilterTypeInterface;
@@ -23,8 +27,12 @@ public function getInnerType(): FilterTypeInterface;
*/
public function getTypeExtensions(): array;
+ public function createBuilder(FilterFactoryInterface $factory, string $name, array $options): FilterBuilderInterface;
+
public function createView(FilterInterface $filter, FilterData $data, DataTableView $parent): FilterView;
+ public function buildFilter(FilterBuilderInterface $builder, array $options): void;
+
public function buildView(FilterView $view, FilterInterface $filter, FilterData $data, array $options): void;
public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter, array $options): void;
diff --git a/src/Filter/Type/SearchFilterType.php b/src/Filter/Type/SearchFilterType.php
old mode 100644
new mode 100755
index 4873505b..77714e4f
--- a/src/Filter/Type/SearchFilterType.php
+++ b/src/Filter/Type/SearchFilterType.php
@@ -7,8 +7,10 @@
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQuery;
use Kreyu\Bundle\DataTableBundle\Filter\FilterData;
use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface;
+use Kreyu\Bundle\DataTableBundle\Filter\Operator;
use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface;
use Symfony\Component\Form\Extension\Core\Type\SearchType;
+use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class SearchFilterType extends AbstractFilterType implements SearchFilterTypeInterface
@@ -25,20 +27,17 @@ public function configureOptions(OptionsResolver $resolver): void
{
$resolver
->setDefaults([
- 'field_type' => SearchType::class,
- 'field_options' => [
- 'attr' => [
- 'placeholder' => 'Search...',
- ],
- ],
+ 'form_type' => SearchType::class,
'label' => false,
- 'operator_options' => [
- 'visible' => false,
- 'choices' => [],
- ],
+ 'supported_operators' => Operator::cases(),
])
->setRequired('handler')
->setAllowedTypes('handler', 'callable')
+ ->addNormalizer('form_options', function (Options $options, array $value): array {
+ return $value + [
+ 'attr' => ($value['attr'] ?? []) + ['placeholder' => 'Search...'],
+ ];
+ })
;
}
}
diff --git a/src/Filter/Type/SearchFilterTypeInterface.php b/src/Filter/Type/SearchFilterTypeInterface.php
old mode 100644
new mode 100755
diff --git a/src/HeaderRowView.php b/src/HeaderRowView.php
old mode 100644
new mode 100755
diff --git a/src/KreyuDataTableBundle.php b/src/KreyuDataTableBundle.php
old mode 100644
new mode 100755
index c96f6bc5..695f9a84
--- a/src/KreyuDataTableBundle.php
+++ b/src/KreyuDataTableBundle.php
@@ -4,6 +4,7 @@
namespace Kreyu\Bundle\DataTableBundle;
+use Kreyu\Bundle\DataTableBundle\DependencyInjection\DataTablePass;
use Kreyu\Bundle\DataTableBundle\DependencyInjection\DefaultConfigurationPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
@@ -12,6 +13,7 @@ class KreyuDataTableBundle extends Bundle
{
public function build(ContainerBuilder $container): void
{
+ $container->addCompilerPass(new DataTablePass());
$container->addCompilerPass(new DefaultConfigurationPass());
}
}
diff --git a/src/Maker/MakeDataTable.php b/src/Maker/MakeDataTable.php
old mode 100644
new mode 100755
diff --git a/src/Pagination/CurrentPageOutOfRangeException.php b/src/Pagination/CurrentPageOutOfRangeException.php
old mode 100644
new mode 100755
diff --git a/src/Pagination/Pagination.php b/src/Pagination/Pagination.php
old mode 100644
new mode 100755
diff --git a/src/Pagination/PaginationData.php b/src/Pagination/PaginationData.php
old mode 100644
new mode 100755
index 78a73dc8..18fdad4f
--- a/src/Pagination/PaginationData.php
+++ b/src/Pagination/PaginationData.php
@@ -23,10 +23,10 @@ public static function fromArray(array $data): self
($resolver = new OptionsResolver())
->setDefault('page', null)
->setDefault('perPage', null)
- ->setNormalizer('page', function (Options $options, mixed $value) {
+ ->addNormalizer('page', function (Options $options, mixed $value) {
return null !== $value ? (int) $value : null;
})
- ->setNormalizer('perPage', function (Options $options, mixed $value) {
+ ->addNormalizer('perPage', function (Options $options, mixed $value) {
return null !== $value ? (int) $value : null;
})
->setAllowedValues('page', function (int $value): bool {
diff --git a/src/Pagination/PaginationInterface.php b/src/Pagination/PaginationInterface.php
old mode 100644
new mode 100755
diff --git a/src/Pagination/PaginationView.php b/src/Pagination/PaginationView.php
old mode 100644
new mode 100755
diff --git a/src/Persistence/CachePersistenceAdapter.php b/src/Persistence/CachePersistenceAdapter.php
old mode 100644
new mode 100755
index e180f7a8..cf3f158f
--- a/src/Persistence/CachePersistenceAdapter.php
+++ b/src/Persistence/CachePersistenceAdapter.php
@@ -60,6 +60,9 @@ private function getCacheKey(DataTableInterface $dataTable, PersistenceSubjectIn
return u(implode('_', array_filter($parts)))->snake()->toString();
}
+ /**
+ * @throws InvalidArgumentException
+ */
private function getCacheValue(string $key, string $tag, mixed $default = null): mixed
{
return $this->cache->get($key, function (ItemInterface $item) use ($tag, $default) {
diff --git a/src/Persistence/CachePersistenceClearer.php b/src/Persistence/CachePersistenceClearer.php
old mode 100644
new mode 100755
diff --git a/src/Persistence/PersistenceAdapterInterface.php b/src/Persistence/PersistenceAdapterInterface.php
old mode 100644
new mode 100755
diff --git a/src/Persistence/PersistenceClearerInterface.php b/src/Persistence/PersistenceClearerInterface.php
old mode 100644
new mode 100755
diff --git a/src/Persistence/PersistenceContext.php b/src/Persistence/PersistenceContext.php
new file mode 100755
index 00000000..2b4b1860
--- /dev/null
+++ b/src/Persistence/PersistenceContext.php
@@ -0,0 +1,13 @@
+add('name', HiddenType::class)
- ->add('order', HiddenType::class)
- ->add('visible', HiddenType::class, [
- 'empty_data' => false,
- ]);
+ ->add('priority', HiddenType::class)
+ ->add('visible', HiddenType::class)
+ ;
+
+ // Add transformer that converts boolean to integer to help the HiddenType visibility field.
+ $builder->get('visible')->addModelTransformer(new CallbackTransformer(
+ fn (?bool $visible) => (int) $visible,
+ fn (int $visible) => (bool) $visible,
+ ));
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => PersonalizationColumnData::class,
+ 'empty_data' => fn (FormInterface $form) => new PersonalizationColumnData($form->getName()),
]);
}
diff --git a/src/Personalization/Form/Type/PersonalizationDataType.php b/src/Personalization/Form/Type/PersonalizationDataType.php
old mode 100644
new mode 100755
index 76eff014..a343c364
--- a/src/Personalization/Form/Type/PersonalizationDataType.php
+++ b/src/Personalization/Form/Type/PersonalizationDataType.php
@@ -40,10 +40,6 @@ public function finishView(FormView $view, FormInterface $form, array $options):
'translation_parameters' => $columnView->vars['translation_parameters'],
]);
}
-
- usort($view['columns']->children, function (FormView $columnA, FormView $columnB) {
- return $columnA->vars['data']->getOrder() <=> $columnB->vars['data']->getOrder();
- });
}
public function configureOptions(OptionsResolver $resolver): void
diff --git a/src/Personalization/PersonalizationColumnData.php b/src/Personalization/PersonalizationColumnData.php
old mode 100644
new mode 100755
index 8f3317df..9796eb58
--- a/src/Personalization/PersonalizationColumnData.php
+++ b/src/Personalization/PersonalizationColumnData.php
@@ -5,57 +5,51 @@
namespace Kreyu\Bundle\DataTableBundle\Personalization;
use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface;
-use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PersonalizationColumnData
{
- public string $name;
- public int $order;
- public bool $visible;
+ private static ?OptionsResolver $optionsResolver = null;
+
+ public function __construct(
+ public string $name,
+ public int $priority = 0,
+ public bool $visible = true,
+ ) {
+ }
/**
* @param array{name: string, order: int, visible: bool} $data
*/
public static function fromArray(array $data): self
{
- ($resolver = new OptionsResolver())
+ $resolver = static::$optionsResolver ??= (new OptionsResolver())
->setRequired('name')
->setDefaults([
- 'order' => 0,
+ 'priority' => 0,
'visible' => true,
])
->setAllowedTypes('name', 'string')
- ->setNormalizer('order', function (Options $options, mixed $value) {
- if (null === $value) {
- return null;
- }
-
- return (int) $value;
- })
- ->setNormalizer('visible', function (Options $options, mixed $value) {
- return (bool) $value;
- })
+ ->setAllowedTypes('priority', 'int')
+ ->setAllowedTypes('visible', 'bool')
;
$data = $resolver->resolve($data);
- $self = new self();
- $self->name = $data['name'];
- $self->order = $data['order'];
- $self->visible = $data['visible'];
-
- return $self;
+ return new self(
+ $data['name'],
+ $data['priority'],
+ $data['visible'],
+ );
}
- public static function fromColumn(ColumnInterface $column, int $order = 0, bool $visible = true): self
+ public static function fromColumn(ColumnInterface $column): self
{
- $self = new self();
- $self->name = $column->getName();
- $self->order = $order;
- $self->visible = $visible;
-
- return $self;
+ return new self(
+ $column->getName(),
+ $column->getPriority(),
+ $column->isVisible(),
+ );
}
public function getName(): string
@@ -63,14 +57,14 @@ public function getName(): string
return $this->name;
}
- public function getOrder(): int
+ public function getPriority(): int
{
- return $this->order;
+ return $this->priority;
}
- public function setOrder(int $order): void
+ public function setPriority(int $priority): void
{
- $this->order = $order;
+ $this->priority = $priority;
}
public function isVisible(): bool
diff --git a/src/Personalization/PersonalizationData.php b/src/Personalization/PersonalizationData.php
old mode 100644
new mode 100755
index c39d984e..86bca8ba
--- a/src/Personalization/PersonalizationData.php
+++ b/src/Personalization/PersonalizationData.php
@@ -6,17 +6,18 @@
use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface;
use Kreyu\Bundle\DataTableBundle\DataTableInterface;
-use Kreyu\Bundle\DataTableBundle\Exception\UnexpectedTypeException;
+use Kreyu\Bundle\DataTableBundle\Exception\InvalidArgumentException;
+use Symfony\Component\OptionsResolver\Options;
+use Symfony\Component\OptionsResolver\OptionsResolver;
class PersonalizationData
{
- /**
- * @var array
- */
+ private static OptionsResolver $optionsResolver;
+
private array $columns = [];
/**
- * @param array $columns
+ * @param array $columns
*/
public function __construct(array $columns = [])
{
@@ -25,197 +26,144 @@ public function __construct(array $columns = [])
}
}
- /**
- * @param array|array> $data
- */
public static function fromArray(array $data): self
{
- $columns = [];
-
- foreach ($data as $key => $value) {
- if (is_array($value)) {
- $value['name'] ??= $key;
- $value = PersonalizationColumnData::fromArray($value);
- } elseif ($value instanceof ColumnInterface) {
- $value = PersonalizationColumnData::fromColumn($value);
- }
-
- $columns[$key] = $value;
- }
+ $resolver = static::$optionsResolver ??= (new OptionsResolver())
+ ->setDefaults([
+ 'columns' => function (OptionsResolver $resolver) {
+ $resolver
+ ->setPrototype(true)
+ ->setDefaults([
+ 'name' => null,
+ 'priority' => 0,
+ 'visible' => true,
+ ])
+ ->setDeprecated('order')
+ ->setAllowedTypes('name', ['null', 'string'])
+ ->setAllowedTypes('priority', 'int')
+ ->setAllowedTypes('visible', 'bool')
+ ;
+ },
+ ])
+ ->addNormalizer('columns', function (Options $options, array $value) {
+ foreach ($value as $name => $column) {
+ $value[$name]['name'] ??= $name;
+ }
+
+ return $value;
+ })
+ ;
+
+ $data = $resolver->resolve($data);
+
+ return new self(array_map(
+ static fn (array $data) => PersonalizationColumnData::fromArray($data),
+ $data['columns'],
+ ));
+ }
- return new self($columns);
+ public static function fromDataTable(DataTableInterface $dataTable): self
+ {
+ return new self(array_filter(
+ $dataTable->getColumns(),
+ static fn (ColumnInterface $column) => $column->getConfig()->isPersonalizable(),
+ ));
}
/**
- * Creates a new instance from a {@see DataTableInterface}.
- * The columns are be added in order they are defined in the data table.
- * Every column is marked as "visible" by default.
+ * @param array $columns
*/
- public static function fromDataTable(DataTableInterface $dataTable): self
+ public function apply(array $columns): void
{
- $columns = [];
+ foreach ($columns as $column) {
+ if (!$column->getConfig()->isPersonalizable()) {
+ continue;
+ }
- foreach (array_values($dataTable->getConfig()->getColumns()) as $index => $column) {
- $columns[] = PersonalizationColumnData::fromColumn($column, $index);
- }
+ if (null === $data = $this->getColumn($column)) {
+ continue;
+ }
- return new self($columns);
+ $column
+ ->setPriority($data->getPriority())
+ ->setVisible($data->isVisible());
+ }
}
- /**
- * Retrieves every defined column personalization data.
- *
- * @return PersonalizationColumnData[]
- */
public function getColumns(): array
{
return $this->columns;
}
- /**
- * Retrieves a column personalization data by its name.
- */
- public function getColumn(string|ColumnInterface $column): ?PersonalizationColumnData
+ public function hasColumn(string|ColumnInterface|PersonalizationColumnData $column): bool
{
- if ($column instanceof ColumnInterface) {
+ if (!is_string($column)) {
+ $column = $column->getName();
+ }
+
+ return array_key_exists($column, $this->columns);
+ }
+
+ public function getColumn(string|ColumnInterface|PersonalizationColumnData $column): ?PersonalizationColumnData
+ {
+ if (!is_string($column)) {
$column = $column->getName();
}
return $this->columns[$column] ?? null;
}
- /**
- * Adds a column personalization data to the stack.
- */
- public function addColumn(PersonalizationColumnData $column): void
+ public function addColumn(ColumnInterface|PersonalizationColumnData $column): void
{
+ if ($column instanceof ColumnInterface) {
+ if (!$column->getConfig()->isPersonalizable()) {
+ throw new InvalidArgumentException('Unable to add non-personalizable column');
+ }
+
+ $column = PersonalizationColumnData::fromColumn($column);
+ }
+
$this->columns[$column->getName()] = $column;
}
- /**
- * Removes a column personalization data from the stack.
- */
- public function removeColumn(PersonalizationColumnData $column): void
+ public function removeColumn(string|ColumnInterface|PersonalizationColumnData $column): void
{
- unset($this->columns[$column->getName()]);
+ if (!is_string($column)) {
+ $column = $column->getName();
+ }
+
+ unset($this->columns[$column]);
}
/**
- * Adds columns not present in the personalization data to the stack.
- *
- * @param array $columns
+ * @param array $columns
*/
- public function addMissingColumns(array $columns, bool $visible = true): void
+ public function addMissingColumns(array $columns): void
{
foreach ($columns as $column) {
- if (null === $this->getColumn($column)) {
- $this->appendColumn($column, $visible);
+ if ($column instanceof ColumnInterface && !$column->getConfig()->isPersonalizable()) {
+ continue;
+ }
+
+ if (!$this->hasColumn($column)) {
+ $this->addColumn($column);
}
}
}
/**
- * Removes columns from the personalization data that does not exist in the given set of columns.
- *
- * @param array $columns
+ * @param array $columns
*/
public function removeRedundantColumns(array $columns): void
{
foreach (array_diff_key($this->columns, $columns) as $column) {
$this->removeColumn($column);
}
- }
- /**
- * Computes given set of {@see ColumnInterface}, ordering it and excluding hidden ones.
- *
- * @param array $columns
- *
- * @return array
- */
- public function compute(array $columns): array
- {
foreach ($columns as $column) {
- if (!$column instanceof ColumnInterface) {
- throw new UnexpectedTypeException($column, ColumnInterface::class);
+ if ($column instanceof ColumnInterface && !$column->getConfig()->isPersonalizable()) {
+ $this->removeColumn($column);
}
}
-
- $columns = array_filter($columns, function (ColumnInterface $column) {
- return $this->isColumnVisible($column);
- });
-
- uasort($columns, function (ColumnInterface $columnA, ColumnInterface $columnB) {
- return $this->getColumnOrder($columnA) <=> $this->getColumnOrder($columnB);
- });
-
- return $columns;
- }
-
- public function getColumnOrder(string|ColumnInterface $column): int
- {
- return $this->getColumn($column)?->getOrder() ?? 0;
- }
-
- public function setColumnOrder(string|ColumnInterface $column, int $order): self
- {
- $this->getColumn($column)?->setOrder($order);
-
- return $this;
- }
-
- public function getColumnVisibility(string|ColumnInterface $column): bool
- {
- return $this->getColumn($column)?->isVisible() ?? true;
- }
-
- public function setColumnVisibility(string|ColumnInterface $column, bool $visible): self
- {
- $this->getColumn($column)?->setVisible($visible);
-
- return $this;
- }
-
- public function isColumnVisible(string|ColumnInterface $column): bool
- {
- return true === $this->getColumnVisibility($column);
- }
-
- public function isColumnHidden(string|ColumnInterface $column): bool
- {
- return false === $this->getColumnVisibility($column);
- }
-
- public function setColumnVisible(string|ColumnInterface $column): self
- {
- return $this->setColumnVisibility($column, true);
- }
-
- public function setColumnHidden(string|ColumnInterface $column): self
- {
- return $this->setColumnVisibility($column, false);
- }
-
- private function appendColumn(string|ColumnInterface $column, bool $visible): void
- {
- $columnsOrders = array_map(
- fn (PersonalizationColumnData $column) => $column->getOrder(),
- array_filter(
- $this->columns,
- fn (PersonalizationColumnData $columnData) => $columnData->isVisible() === $visible,
- ),
- );
-
- $columnOrder = 0;
-
- if (!empty($columnsOrders)) {
- $columnOrder = max($columnsOrders) + 1;
- }
-
- $this->columns[$column->getName()] = PersonalizationColumnData::fromColumn(
- column: $column,
- order: $columnOrder,
- visible: $visible,
- );
}
}
diff --git a/src/Query/ChainProxyQueryFactory.php b/src/Query/ChainProxyQueryFactory.php
old mode 100644
new mode 100755
index 88966156..4905587e
--- a/src/Query/ChainProxyQueryFactory.php
+++ b/src/Query/ChainProxyQueryFactory.php
@@ -4,6 +4,7 @@
namespace Kreyu\Bundle\DataTableBundle\Query;
+use Kreyu\Bundle\DataTableBundle\Exception\InvalidArgumentException;
use Kreyu\Bundle\DataTableBundle\Exception\UnexpectedTypeException;
class ChainProxyQueryFactory implements ProxyQueryFactoryInterface
@@ -12,7 +13,7 @@ class ChainProxyQueryFactory implements ProxyQueryFactoryInterface
* @param array $factories
*/
public function __construct(
- private iterable $factories,
+ private readonly iterable $factories,
) {
}
@@ -25,6 +26,6 @@ public function create(mixed $data): ProxyQueryInterface
}
}
- throw new \InvalidArgumentException('Unable to create ProxyQuery class for given data');
+ throw new InvalidArgumentException(sprintf('Unable to create proxy query for data of type "%s"', get_debug_type($data)));
}
}
diff --git a/src/Query/ProxyQueryFactoryInterface.php b/src/Query/ProxyQueryFactoryInterface.php
old mode 100644
new mode 100755
diff --git a/src/Query/ProxyQueryInterface.php b/src/Query/ProxyQueryInterface.php
old mode 100644
new mode 100755
index fe8ad37a..baaa1b0c
--- a/src/Query/ProxyQueryInterface.php
+++ b/src/Query/ProxyQueryInterface.php
@@ -15,4 +15,6 @@ public function sort(SortingData $sortingData): void;
public function paginate(PaginationData $paginationData): void;
public function getPagination(): PaginationInterface;
+
+ public function getItems(): iterable;
}
diff --git a/src/Request/HttpFoundationRequestHandler.php b/src/Request/HttpFoundationRequestHandler.php
old mode 100644
new mode 100755
index 06366cb4..a5b9de8d
--- a/src/Request/HttpFoundationRequestHandler.php
+++ b/src/Request/HttpFoundationRequestHandler.php
@@ -48,7 +48,7 @@ private function filter(DataTableInterface $dataTable, Request $request): void
$form = $dataTable->createFiltrationFormBuilder()->getForm();
$form->handleRequest($request);
- if ($form->isSubmitted()) {
+ if ($form->isSubmitted() && $form->isValid()) {
$dataTable->filter($form->getData());
}
}
@@ -102,7 +102,7 @@ private function personalize(DataTableInterface $dataTable, Request $request): v
$form = $dataTable->createPersonalizationFormBuilder()->getForm();
$form->handleRequest($request);
- if ($form->isSubmitted()) {
+ if ($form->isSubmitted() && $form->isValid()) {
$dataTable->personalize($form->getData());
}
}
@@ -116,8 +116,8 @@ private function export(DataTableInterface $dataTable, Request $request): void
$form = $dataTable->createExportFormBuilder()->getForm();
$form->handleRequest($request);
- if ($form->isSubmitted()) {
- $dataTable->export($form->getData());
+ if ($form->isSubmitted() && $form->isValid()) {
+ $dataTable->setExportData($form->getData());
}
}
diff --git a/src/Request/RequestHandlerInterface.php b/src/Request/RequestHandlerInterface.php
old mode 100644
new mode 100755
diff --git a/src/Resources/config/actions.php b/src/Resources/config/actions.php
old mode 100644
new mode 100755
index 5f76912b..58249fdd
--- a/src/Resources/config/actions.php
+++ b/src/Resources/config/actions.php
@@ -28,8 +28,7 @@
$services
->set('kreyu_data_table.action.registry', ActionRegistry::class)
->args([
- tagged_iterator('kreyu_data_table.action.type'),
- tagged_iterator('kreyu_data_table.action.type_extension'),
+ tagged_iterator('kreyu_data_table.action.extension'),
service('kreyu_data_table.action.resolved_type_factory'),
])
->alias(ActionRegistryInterface::class, 'kreyu_data_table.action.registry')
diff --git a/src/Resources/config/columns.php b/src/Resources/config/columns.php
old mode 100644
new mode 100755
index 06e418a4..19bc46ba
--- a/src/Resources/config/columns.php
+++ b/src/Resources/config/columns.php
@@ -11,6 +11,7 @@
use Kreyu\Bundle\DataTableBundle\Column\Type\CheckboxColumnType;
use Kreyu\Bundle\DataTableBundle\Column\Type\CollectionColumnType;
use Kreyu\Bundle\DataTableBundle\Column\Type\ColumnType;
+use Kreyu\Bundle\DataTableBundle\Column\Type\DateColumnType;
use Kreyu\Bundle\DataTableBundle\Column\Type\DatePeriodColumnType;
use Kreyu\Bundle\DataTableBundle\Column\Type\DateTimeColumnType;
use Kreyu\Bundle\DataTableBundle\Column\Type\FormColumnType;
@@ -37,8 +38,7 @@
$services
->set('kreyu_data_table.column.registry', ColumnRegistry::class)
->args([
- tagged_iterator('kreyu_data_table.column.type'),
- tagged_iterator('kreyu_data_table.column.type_extension'),
+ tagged_iterator('kreyu_data_table.column.extension'),
service('kreyu_data_table.column.resolved_type_factory'),
])
->alias(ColumnRegistryInterface::class, 'kreyu_data_table.column.registry')
@@ -52,15 +52,16 @@
$services
->set('kreyu_data_table.column.type.column', ColumnType::class)
+ ->args([service('translator')->nullOnInvalid()])
->tag('kreyu_data_table.column.type')
;
$services
->set('kreyu_data_table.column.type.actions', ActionsColumnType::class)
- ->tag('kreyu_data_table.column.type')
->args([
service('kreyu_data_table.action.factory'),
])
+ ->tag('kreyu_data_table.column.type')
;
$services
@@ -76,7 +77,6 @@
$services
->set('kreyu_data_table.column.type.collection', CollectionColumnType::class)
->tag('kreyu_data_table.column.type')
- ->call('setColumnFactory', [service('kreyu_data_table.column.factory')])
;
$services
@@ -109,6 +109,11 @@
->tag('kreyu_data_table.column.type')
;
+ $services
+ ->set('kreyu_data_table.column.type.date', DateColumnType::class)
+ ->tag('kreyu_data_table.column.type')
+ ;
+
$services
->set('kreyu_data_table.column.type.date_period', DatePeriodColumnType::class)
->tag('kreyu_data_table.column.type')
diff --git a/src/Resources/config/core.php b/src/Resources/config/core.php
old mode 100644
new mode 100755
index 9355b145..32d6668e
--- a/src/Resources/config/core.php
+++ b/src/Resources/config/core.php
@@ -15,12 +15,12 @@
use Kreyu\Bundle\DataTableBundle\Query\ChainProxyQueryFactory;
use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryFactoryInterface;
use Kreyu\Bundle\DataTableBundle\Request\HttpFoundationRequestHandler;
-use Kreyu\Bundle\DataTableBundle\Request\RequestHandlerInterface;
use Kreyu\Bundle\DataTableBundle\Type\DataTableType;
use Kreyu\Bundle\DataTableBundle\Type\ResolvedDataTableTypeFactory;
use Kreyu\Bundle\DataTableBundle\Type\ResolvedDataTableTypeFactoryInterface;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
+use function Symfony\Component\DependencyInjection\Loader\Configurator\abstract_arg;
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
use function Symfony\Component\DependencyInjection\Loader\Configurator\tagged_iterator;
@@ -34,14 +34,14 @@
$services
->set('kreyu_data_table.type.data_table', DataTableType::class)
+ ->arg('$defaults', abstract_arg('Default options, provided by KreyuDataTableExtension and DefaultConfigurationPass'))
->tag('kreyu_data_table.type')
;
$services
->set('kreyu_data_table.registry', DataTableRegistry::class)
->args([
- tagged_iterator('kreyu_data_table.type'),
- tagged_iterator('kreyu_data_table.type_extension'),
+ tagged_iterator('kreyu_data_table.extension'),
service('kreyu_data_table.resolved_type_factory'),
])
->alias(DataTableRegistryInterface::class, 'kreyu_data_table.registry')
@@ -67,7 +67,6 @@
$services
->set('kreyu_data_table.request_handler.http_foundation', HttpFoundationRequestHandler::class)
- ->alias(RequestHandlerInterface::class, 'kreyu_data_table.request_handler.http_foundation')
;
$services
diff --git a/src/Resources/config/exporter.php b/src/Resources/config/exporter.php
old mode 100644
new mode 100755
index 5de88004..7359e345
--- a/src/Resources/config/exporter.php
+++ b/src/Resources/config/exporter.php
@@ -15,6 +15,7 @@
use Kreyu\Bundle\DataTableBundle\Exporter\ExporterFactoryInterface;
use Kreyu\Bundle\DataTableBundle\Exporter\ExporterRegistry;
use Kreyu\Bundle\DataTableBundle\Exporter\ExporterRegistryInterface;
+use Kreyu\Bundle\DataTableBundle\Exporter\Type\CallbackExporterType;
use Kreyu\Bundle\DataTableBundle\Exporter\Type\ExporterType;
use Kreyu\Bundle\DataTableBundle\Exporter\Type\ResolvedExporterTypeFactory;
use Kreyu\Bundle\DataTableBundle\Exporter\Type\ResolvedExporterTypeFactoryInterface;
@@ -34,7 +35,7 @@
$services
->set('kreyu_data_table.exporter.registry', ExporterRegistry::class)
->args([
- tagged_iterator('kreyu_data_table.exporter.type'),
+ tagged_iterator('kreyu_data_table.exporter.extension'),
service('kreyu_data_table.exporter.resolved_type_factory'),
])
->alias(ExporterRegistryInterface::class, 'kreyu_data_table.exporter.registry')
@@ -51,6 +52,11 @@
->tag('kreyu_data_table.exporter.type')
;
+ $services
+ ->set('kreyu_data_table.exporter.type.callback', CallbackExporterType::class)
+ ->tag('kreyu_data_table.exporter.type')
+ ;
+
$services
->set('kreyu_data_table.exporter.type.open_spout.abstract', OpenSpout\AbstractExporterType::class)
->abstract()
diff --git a/src/Resources/config/extensions.php b/src/Resources/config/extensions.php
old mode 100644
new mode 100755
index 3899c35e..35d9cd93
--- a/src/Resources/config/extensions.php
+++ b/src/Resources/config/extensions.php
@@ -2,14 +2,71 @@
declare(strict_types=1);
-use Kreyu\Bundle\DataTableBundle\Extension\Core\DefaultConfigurationDataTableTypeExtension;
+use Kreyu\Bundle\DataTableBundle\Action\Extension\DependencyInjection\DependencyInjectionActionExtension;
+use Kreyu\Bundle\DataTableBundle\Column\Extension\DependencyInjection\DependencyInjectionColumnExtension;
+use Kreyu\Bundle\DataTableBundle\Exporter\Extension\DependencyInjection\DependencyInjectionExporterExtension;
+use Kreyu\Bundle\DataTableBundle\Extension\DependencyInjection\DependencyInjectionDataTableExtension;
+use Kreyu\Bundle\DataTableBundle\Extension\HttpFoundation\HttpFoundationDataTableTypeExtension;
+use Kreyu\Bundle\DataTableBundle\Filter\Extension\DependencyInjection\DependencyInjectionFilterExtension;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
+use function Symfony\Component\DependencyInjection\Loader\Configurator\abstract_arg;
+use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
+
return static function (ContainerConfigurator $configurator) {
- $services = $configurator->services();
+ $configurator->services()
+ ->set('kreyu_data_table.extension', DependencyInjectionDataTableExtension::class)
+ ->args([
+ abstract_arg('All services with tag "kreyu_data_table.type" are stored in a service locator by DataTablePass'),
+ abstract_arg('All services with tag "kreyu_data_table.type_extension" are stored here by DataTablePass'),
+ ])
+ ->tag('kreyu_data_table.extension', [
+ 'type' => 'kreyu_data_table.type',
+ 'type_extension' => 'kreyu_data_table.type_extension',
+ ])
+
+ ->set('kreyu_data_table.column.extension', DependencyInjectionColumnExtension::class)
+ ->args([
+ abstract_arg('All services with tag "kreyu_data_table.column.type" are stored in a service locator by DataTablePass'),
+ abstract_arg('All services with tag "kreyu_data_table.column.type_extension" are stored here by DataTablePass'),
+ ])
+ ->tag('kreyu_data_table.column.extension', [
+ 'type' => 'kreyu_data_table.column.type',
+ 'type_extension' => 'kreyu_data_table.column.type_extension',
+ ])
+
+ ->set('kreyu_data_table.filter.extension', DependencyInjectionFilterExtension::class)
+ ->args([
+ abstract_arg('All services with tag "kreyu_data_table.filter.type" are stored in a service locator by DataTablePass'),
+ abstract_arg('All services with tag "kreyu_data_table.filter.type_extension" are stored here by DataTablePass'),
+ ])
+ ->tag('kreyu_data_table.filter.extension', [
+ 'type' => 'kreyu_data_table.filter.type',
+ 'type_extension' => 'kreyu_data_table.filter.type_extension',
+ ])
+
+ ->set('kreyu_data_table.exporter.extension', DependencyInjectionExporterExtension::class)
+ ->args([
+ abstract_arg('All services with tag "kreyu_data_table.exporter.type" are stored in a service locator by DataTablePass'),
+ abstract_arg('All services with tag "kreyu_data_table.exporter.type_extension" are stored here by DataTablePass'),
+ ])
+ ->tag('kreyu_data_table.exporter.extension', [
+ 'type' => 'kreyu_data_table.exporter.type',
+ 'type_extension' => 'kreyu_data_table.exporter.type_extension',
+ ])
+
+ ->set('kreyu_data_table.action.extension', DependencyInjectionActionExtension::class)
+ ->args([
+ abstract_arg('All services with tag "kreyu_data_table.action.type" are stored in a service locator by DataTablePass'),
+ abstract_arg('All services with tag "kreyu_data_table.action.type_extension" are stored here by DataTablePass'),
+ ])
+ ->tag('kreyu_data_table.action.extension', [
+ 'type' => 'kreyu_data_table.action.type',
+ 'type_extension' => 'kreyu_data_table.action.type_extension',
+ ])
- $services
- ->set('kreyu_data_table.type_extension.default_configuration', DefaultConfigurationDataTableTypeExtension::class)
- ->tag('kreyu_data_table.type_extension', ['priority' => 999])
+ ->set('kreyu_data_table.type_extension.http_foundation', HttpFoundationDataTableTypeExtension::class)
+ ->args([service('kreyu_data_table.request_handler.http_foundation')])
+ ->tag('kreyu_data_table.type_extension')
;
};
diff --git a/src/Resources/config/filtration.php b/src/Resources/config/filtration.php
old mode 100644
new mode 100755
index cb59d5c6..fdb54661
--- a/src/Resources/config/filtration.php
+++ b/src/Resources/config/filtration.php
@@ -7,6 +7,7 @@
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\DateFilterType;
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\DateRangeFilterType;
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\DateTimeFilterType;
+use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\DoctrineOrmFilterType;
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\EntityFilterType;
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\NumericFilterType;
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\StringFilterType;
@@ -49,8 +50,7 @@
$services
->set('kreyu_data_table.filter.registry', FilterRegistry::class)
->args([
- tagged_iterator('kreyu_data_table.filter.type'),
- tagged_iterator('kreyu_data_table.filter.type_extension'),
+ tagged_iterator('kreyu_data_table.filter.extension'),
service('kreyu_data_table.filter.resolved_type_factory'),
])
->alias(FilterRegistryInterface::class, 'kreyu_data_table.filter.registry')
@@ -72,6 +72,13 @@
->tag('kreyu_data_table.filter.type')
;
+ // Doctrine ORM
+
+ $services
+ ->set('kreyu_data_table.filter.type.doctrine_orm', DoctrineOrmFilterType::class)
+ ->tag('kreyu_data_table.filter.type')
+ ;
+
$services
->set('kreyu_data_table.filter.type.doctrine_orm_string', StringFilterType::class)
->tag('kreyu_data_table.filter.type')
@@ -84,6 +91,7 @@
$services
->set('kreyu_data_table.filter.type.doctrine_orm_entity', EntityFilterType::class)
+ ->args([service('doctrine')])
->tag('kreyu_data_table.filter.type')
;
diff --git a/src/Resources/config/personalization.php b/src/Resources/config/personalization.php
old mode 100644
new mode 100755
diff --git a/src/Resources/config/twig.php b/src/Resources/config/twig.php
old mode 100644
new mode 100755
diff --git a/src/Resources/help/MakeDataTable.txt b/src/Resources/help/MakeDataTable.txt
old mode 100644
new mode 100755
diff --git a/src/Resources/skeleton/DataTableType.tpl.php b/src/Resources/skeleton/DataTableType.tpl.php
old mode 100644
new mode 100755
diff --git a/src/Resources/translations/KreyuDataTable.en.yaml b/src/Resources/translations/KreyuDataTable.en.yaml
old mode 100644
new mode 100755
index 4204e639..b62723ae
--- a/src/Resources/translations/KreyuDataTable.en.yaml
+++ b/src/Resources/translations/KreyuDataTable.en.yaml
@@ -1,14 +1,14 @@
# Operators
-EQUALS: equals
-CONTAINS: contains
-NOT_CONTAINS: not contains
-NOT_EQUALS: not equals
-GREATER_THAN_EQUALS: greater or equal
-LESS_THAN_EQUALS: lesser or equal
-GREATER_THAN: greater than
-LESS_THAN: lesser than
-STARTS_WITH: starts with
-ENDS_WITH: ends with
+Equals: equals
+Not equals: not equals
+Contains: contains
+Not contains: not contains
+Greater than: greater than
+Greater than or equals: greater than or equals
+Less than: less than
+Less than or equals: less than or equals
+Starts with: starts with
+Ends with: ends with
# Boolean type
Yes: Yes
@@ -35,8 +35,8 @@ Strategy: Strategy
Include personalization: Include personalization
# Export strategy
-INCLUDE_CURRENT_PAGE: Include current page
-INCLUDE_ALL: From all pages
+Include current page: Include current page
+Include all: From all pages
# Misc
Selected: Selected
diff --git a/src/Resources/translations/KreyuDataTable.pl.yaml b/src/Resources/translations/KreyuDataTable.pl.yaml
old mode 100644
new mode 100755
index f32d62b1..49372132
--- a/src/Resources/translations/KreyuDataTable.pl.yaml
+++ b/src/Resources/translations/KreyuDataTable.pl.yaml
@@ -1,14 +1,14 @@
# Operators
-EQUALS: równa się
-CONTAINS: zawiera
-NOT_CONTAINS: nie zawiera
-NOT_EQUALS: różne od
-GREATER_THAN_EQUALS: większe lub równe
-LESS_THAN_EQUALS: mniejsze lub równe
-GREATER_THAN: większe od
-LESS_THAN: mniejsze od
-STARTS_WITH: zaczyna się od
-ENDS_WITH: kończy się z
+Equals: równa się
+Not equals: różne od
+Contains: zawiera
+Not contains: nie zawiera
+Greater than: większe od
+Greater than or equals: większe lub równe
+Less than: mniejsze od
+Less than or equals: mniejsze lub równe
+Starts with: zaczyna się od
+Ends with: kończy się z
# Boolean type
Yes: Tak
@@ -35,8 +35,8 @@ Strategy: Uwzględnij wyniki
Include personalization: Uwzględnij personalizację
# Export strategy
-INCLUDE_CURRENT_PAGE: Z aktualnej strony
-INCLUDE_ALL: Z wszystkich stron
+Include current page: Z aktualnej strony
+Include all: Z wszystkich stron
# Misc
Selected: Zaznaczono
diff --git a/src/Resources/views/macros.html.twig b/src/Resources/views/macros.html.twig
old mode 100644
new mode 100755
index dfefc8f2..e318d193
--- a/src/Resources/views/macros.html.twig
+++ b/src/Resources/views/macros.html.twig
@@ -3,9 +3,7 @@
app.request.get('_route'),
app.request.attributes.get('_route_params')|merge(app.request.query.all)|merge({
(filtration_parameter_name): app.request.query.all(filtration_parameter_name)|merge({
- (filter.vars.name): {
- value: ''
- }
+ (filter.vars.name): filter.vars.clear_filter_parameters
})
})
) }}
@@ -16,9 +14,7 @@
{% for filter in filters %}
{% set query = query|merge({
- (filter.vars.name): {
- value: ''
- }
+ (filter.vars.name): filter.vars.clear_filter_parameters
}) %}
{% endfor %}
diff --git a/src/Resources/views/themes/base.html.twig b/src/Resources/views/themes/base.html.twig
old mode 100644
new mode 100755
index ddabc3a5..819fffe9
--- a/src/Resources/views/themes/base.html.twig
+++ b/src/Resources/views/themes/base.html.twig
@@ -291,7 +291,7 @@
{% endblock %}
-{% block kreyu_data_table_date_range_row %}
+{% block kreyu_data_table_date_range_widget %}
{{ form_widget(form.from) }}
{{ form_widget(form.to) }}
{% endblock %}
@@ -301,7 +301,7 @@
{% block column_header %}
{% set label_attr = label_attr|default({}) %}
- {% if data_table.vars.sorting_enabled and sort_field %}
+ {% if data_table.vars.sorting_enabled and sortable %}
{% set current_sort_field = sorting_field_data.name|default(null) %}
{% set current_sort_direction = sorting_field_data.direction|default(null) %}
@@ -320,6 +320,7 @@
{% set query_params = app.request.attributes.get('_route_params')|merge(query_params) %}
{% set label_attr = { href: path(app.request.get('_route'), query_params) }|merge(label_attr) %}
+ {% set label_attr = { 'data-turbo-action': 'advance' }|merge(label_attr) %}
{{- block('column_header_label', theme, _context) -}}
@@ -373,7 +374,7 @@
{# @var array intl_formatter_options - Options used to configure the Intl formatter #}
{% if use_intl_formatter %}
- {% set value = value|format_number(intl_formatter_options.attrs, intl_formatter_options.style) %}
+ {#{% set value = value|format_number(intl_formatter_options.attrs, intl_formatter_options.style) %}#}
{% endif %}
{{- block('column_text_value') -}}
@@ -385,7 +386,7 @@
{# @var string currency #}
{% if use_intl_formatter %}
- {% set value = value|format_currency(currency, intl_formatter_options.attrs) %}
+ {#{% set value = value|format_currency(currency, intl_formatter_options.attrs) %}#}
{% endif %}
{{- block('column_text_value') -}}
@@ -420,10 +421,14 @@
{% endblock %}
{% block column_collection_value %}
- {% for child in children %}
- {{- data_table_column_value(child) -}}
- {% if not loop.last %}{{- separator -}}{% endif %}
- {% endfor %}
+ {# Note: do **not** remove the HTML comments below, because it makes sure the separator is rendered properly. #}
+ {% apply spaceless %}
+ {% for child in children %}
+ {% if not loop.first %}-->{% endif %}
+ {{- data_table_column_value(child) -}}{% if not loop.last %}{{- separator -}}