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 -}}