From d95cba9f56ae6acc0886324e1127b4956ae58f43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Wr=C3=B3blewski?= Date: Sun, 21 Jan 2024 22:18:28 +0100 Subject: [PATCH] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 943da3b2cbf081f6fe91544fda29178c7f2672be Merge: a45165cb 0a2bb9fc Author: Sebastian Wróblewski Date: Sun Jan 21 22:14:13 2024 +0100 Merge branch 'main' of https://github.com/Kreyu/data-table-bundle into feature/testing/filters commit a45165cb46d93fc3ee9d6b32d58fb969a9ed63e0 Author: Sebastian Wróblewski Date: Sun Jan 21 22:03:11 2024 +0100 wip commit e351b386d6b7ac9155e60b8a83d896a0e5196db4 Author: Sebastian Wróblewski Date: Sun Jan 21 20:36:39 2024 +0100 wip commit 54eb3548403216b04b70587c19e801bed591c1b6 Author: Sebastian Wróblewski Date: Sun Jan 21 19:34:00 2024 +0100 wip commit ebebb9b8f15f822e9b2f87a3c3b09f8eb007e18c Author: Sebastian Wróblewski Date: Sun Jan 21 19:31:10 2024 +0100 wip commit 63a22c6b24189023775d334179bccc92d974b809 Author: Sebastian Wróblewski Date: Sun Jan 21 19:30:00 2024 +0100 wip commit 67c3558d0f7408117c7b7e3b8250de7ab35c75ab Author: Sebastian Wróblewski Date: Sun Jan 21 19:23:14 2024 +0100 wip commit 5986098e174d59c99cd8e44899fa800369adc04e Author: Sebastian Wróblewski Date: Sat Jan 13 14:06:45 2024 +0100 wip commit e0c989f1a486e1d7391854e629dfaf7a5ae51d6e Author: Sebastian Wróblewski Date: Sat Jan 13 13:57:47 2024 +0100 wip commit e2939a09d2a53345fb8831118a6768ddd9fb9a05 Author: Sebastian Wróblewski Date: Tue Jan 2 22:10:05 2024 +0100 wip commit e4cd4f9e4b81de5ba8c4565d568807f105c25c8e Author: Sebastian Wróblewski Date: Tue Jan 2 21:59:28 2024 +0100 wip commit e8cc9c34bf63544f0d814defb2b83191f984593c Author: Sebastian Wróblewski Date: Tue Jan 2 21:39:07 2024 +0100 wip commit 53a1fd03ba0b80a3215da8c4600a976411208730 Author: Sebastian Wróblewski Date: Tue Jan 2 00:00:45 2024 +0100 wip commit 5b82994b23536ba606df4daa9bec8b666ef3de22 Author: Sebastian Wróblewski Date: Sat Dec 30 15:20:44 2023 +0100 wip commit 490d233930013eb0edf9f7e7ad1ed0355b85f0b6 Author: Sebastian Wróblewski Date: Sat Dec 30 15:14:24 2023 +0100 wip commit 42b0fa306e5a1f88f9e06317728caf73bd9f8a3e Author: Sebastian Wróblewski Date: Fri Dec 29 18:58:01 2023 +0100 wip commit 14393a0ef6a6bcc6a70e3c2960a887313b7eee74 Author: Sebastian Wróblewski Date: Fri Dec 29 00:26:30 2023 +0100 wip commit 519ace903ee8cc87b0b7aefe68f80a594abd8b66 Author: Sebastian Wróblewski Date: Thu Dec 28 22:17:33 2023 +0100 wip commit fbca593977ace0d6fe7d853bbd2851db4a81ffa0 Author: Sebastian Wróblewski Date: Thu Dec 28 01:03:17 2023 +0100 wip commit 9aa831dd5eb297306fb04d9a403ea38079232505 Author: Sebastian Wróblewski Date: Wed Dec 27 18:39:57 2023 +0100 wip commit 5e7a4a9227e69747f6ed1470ffd824fad7b711a6 Author: Sebastian Wróblewski Date: Fri Dec 22 12:50:26 2023 +0100 wip commit bc77417ddd1c8d87aefbd9acee5265109a3839e7 Author: Sebastian Wróblewski Date: Sat Dec 16 20:57:21 2023 +0100 wip commit e68466e5480d4a9d1a1a380f07351a5b798a117b Author: Sebastian Wróblewski Date: Mon Dec 4 00:44:36 2023 +0100 wip commit 4a8cc7b74b7f44297a9c35a5570ef302f13d49cd Author: Sebastian Wróblewski Date: Mon Dec 4 00:43:37 2023 +0100 wip commit f5b3d6327f41473aa4e462654f1fe791aed2ddc9 Author: Sebastian Wróblewski Date: Mon Dec 4 00:42:39 2023 +0100 wip commit a312f4784507ebea7239602903b6825fc2623313 Author: Sebastian Wróblewski Date: Tue Nov 28 19:48:13 2023 +0100 wip commit 5604a34bcedd51c65d5218b00cec2a405b31529e Author: Sebastian Wróblewski Date: Tue Nov 28 18:06:49 2023 +0100 wip commit 490e6c69e7b76d79254223f4c4582af7651b2b06 Author: Sebastian Wróblewski Date: Sat Nov 25 13:42:45 2023 +0100 fix open spout docs commit 5a34957aac0ef678315521efae8523ec3b62cc83 Merge: a2697008 166f7b3e Author: Sebastian Wróblewski Date: Thu Nov 23 00:20:21 2023 +0100 Merge branch 'main' of https://github.com/Kreyu/data-table-bundle into feature/testing/filters commit a2697008d4b217405acac8b7dbdcf0fc2a6f5ab4 Author: Sebastian Wróblewski Date: Wed Nov 22 22:45:26 2023 +0100 wip --- .github/workflows/retype-action.yml | 18 - .github/workflows/testing.yml | 20 + .gitignore | 1 + CHANGELOG.md | 13 +- composer.json | 10 +- docs/.gitignore | 3 + docs/.vitepress/config.mts | 175 ++ docs/.vitepress/theme/index.js | 4 + docs/.vitepress/theme/style.css | 30 + docs/README.md | 7 + docs/basic-usage/adding-actions.md | 207 --- docs/basic-usage/adding-columns.md | 65 - docs/basic-usage/creating-data-tables.md | 96 - docs/basic-usage/defining-the-filters.md | 41 - docs/basic-usage/disclaimer.md | 52 - docs/basic-usage/enabling-global-search.md | 65 - docs/basic-usage/enabling-persistence.md | 38 - docs/basic-usage/exporting-the-data.md | 106 -- docs/basic-usage/index.yml | 2 - docs/basic-usage/internationalization.md | 87 - docs/basic-usage/persisting-applied-data.md | 48 - docs/basic-usage/rendering-the-table.md | 114 -- docs/basic-usage/summary.md | 16 - docs/contributing.md | 18 - docs/features/actions/actions.md | 9 - docs/features/actions/batch-actions.md | 405 ----- docs/features/actions/global-actions.md | 272 --- docs/features/actions/index.yml | 1 - docs/features/actions/row-actions.md | 301 ---- docs/features/columns.md | 180 -- docs/features/filtering.md | 413 ----- docs/features/global-search.md | 87 - docs/features/index.yml | 3 - docs/features/proxy-queries.md | 167 -- docs/features/request-handlers.md | 130 -- docs/features/type-classes.md | 344 ---- docs/index.md | 23 - docs/package-lock.json | 1571 +++++++++++++++++ docs/package.json | 12 + docs/reference/actions/index.yml | 2 - docs/reference/actions/types.md | 14 - docs/reference/actions/types/action.md | 20 - docs/reference/actions/types/link.md | 60 - docs/reference/columns/index.yml | 2 - docs/reference/columns/types.md | 25 - docs/reference/columns/types/actions.md | 74 - docs/reference/columns/types/boolean.md | 36 - docs/reference/columns/types/checkbox.md | 36 - docs/reference/columns/types/column.md | 20 - docs/reference/columns/types/date-period.md | 36 - docs/reference/columns/types/date-time.md | 29 - docs/reference/columns/types/date.md | 31 - docs/reference/columns/types/form.md | 39 - docs/reference/columns/types/template.md | 34 - docs/reference/columns/types/text.md | 24 - docs/reference/exporters/index.yml | 2 - docs/reference/exporters/types.md | 21 - docs/reference/exporters/types/exporter.md | 18 - .../types/open-spout/_open-spout_options.md | 101 -- .../exporters/types/open-spout/csv.md | 52 - .../exporters/types/open-spout/index.yml | 1 - .../exporters/types/open-spout/ods.md | 53 - .../exporters/types/open-spout/open-spout.md | 25 - .../exporters/types/php-spreadsheet/html.md | 22 - .../exporters/types/php-spreadsheet/index.yml | 1 - .../exporters/types/php-spreadsheet/ods.md | 22 - .../types/php-spreadsheet/php-spreadsheet.md | 18 - .../exporters/types/php-spreadsheet/xls.md | 22 - .../exporters/types/php-spreadsheet/xlsx.md | 33 - docs/reference/filters/index.yml | 2 - docs/reference/filters/types.md | 20 - .../filters/types/_filter_options.md | 104 -- .../_doctrine_orm_filter_options.md | 8 - .../filters/types/doctrine-orm/boolean.md | 34 - .../filters/types/doctrine-orm/callback.md | 62 - .../filters/types/doctrine-orm/date-time.md | 53 - .../filters/types/doctrine-orm/date.md | 45 - .../types/doctrine-orm/doctrine-orm.md | 40 - .../filters/types/doctrine-orm/entity.md | 36 - .../filters/types/doctrine-orm/index.yml | 1 - .../filters/types/doctrine-orm/numeric.md | 34 - .../filters/types/doctrine-orm/string.md | 32 - docs/reference/filters/types/filter.md | 24 - docs/reference/filters/types/search.md | 71 - docs/reference/index.yml | 2 - docs/retype.yml | 29 - docs/src/docs/components/actions.md | 571 ++++++ docs/src/docs/components/columns.md | 276 +++ docs/src/docs/components/exporters.md | 74 + docs/src/docs/components/filters.md | 394 +++++ docs/src/docs/contributing.md | 13 + .../docs/features/asynchronicity.md} | 28 +- docs/{ => src/docs}/features/exporting.md | 110 +- docs/src/docs/features/extensibility.md | 270 +++ docs/src/docs/features/filtering.md | 225 +++ docs/{ => src/docs}/features/pagination.md | 80 +- docs/{ => src/docs}/features/persistence.md | 211 ++- .../docs}/features/personalization.md | 88 +- docs/{ => src/docs}/features/sorting.md | 88 +- docs/{ => src/docs}/features/theming.md | 54 +- docs/{ => src/docs}/installation.md | 58 +- .../docs/integrations/doctrine-orm/events.md | 75 + .../doctrine-orm/expression-transformers.md | 241 +++ docs/src/docs/introduction.md | 106 ++ docs/{ => src/docs}/troubleshooting.md | 46 +- docs/src/index.md | 45 + .../public}/action_confirmation_modal.png | Bin docs/{static => src/public}/export_modal.png | Bin docs/src/public/logo.png | Bin 0 -> 30120 bytes .../public}/personalization_modal.png | Bin .../public/search_filter_type.png} | Bin docs/{ => src}/reference/configuration.md | 33 +- docs/{ => src}/reference/twig.md | 49 +- docs/src/reference/types/action.md | 8 + docs/src/reference/types/action/action.md | 11 + .../reference/types/action}/button.md | 25 +- .../reference/types/action}/form.md | 27 +- docs/src/reference/types/action/link.md | 47 + .../reference/types/action/options/action.md} | 92 +- docs/src/reference/types/column.md | 20 + docs/src/reference/types/column/actions.md | 74 + docs/src/reference/types/column/boolean.md | 27 + docs/src/reference/types/column/checkbox.md | 32 + .../reference/types/column}/collection.md | 29 +- docs/src/reference/types/column/column.md | 11 + .../src/reference/types/column/date-period.md | 34 + docs/src/reference/types/column/date-time.md | 27 + docs/src/reference/types/column/date.md | 29 + .../reference/types/column}/link.md | 23 +- .../reference/types/column}/money.md | 21 +- .../reference/types/column}/number.md | 23 +- .../reference/types/column/options/column.md} | 59 +- docs/src/reference/types/column/template.md | 25 + docs/src/reference/types/column/text.md | 15 + docs/src/reference/types/data-table.md | 150 ++ docs/src/reference/types/exporter.md | 25 + docs/src/reference/types/exporter/callback.md | 34 + docs/src/reference/types/exporter/exporter.md | 11 + .../types/exporter/open-spout/csv.md | 41 + .../types/exporter/open-spout/ods.md | 42 + .../types/exporter}/open-spout/xlsx.md | 23 +- .../types/exporter/options/exporter.md} | 6 +- .../exporter/options/php-spreadsheet.md} | 4 +- .../types/exporter}/php-spreadsheet/csv.md | 25 +- .../types/exporter/php-spreadsheet/html.md} | 34 +- .../types/exporter/php-spreadsheet/ods.md | 17 + .../types/exporter/php-spreadsheet/pdf.md | 120 ++ .../types/exporter/php-spreadsheet/xls.md | 17 + .../types/exporter/php-spreadsheet/xlsx.md | 23 + docs/src/reference/types/filter.md | 20 + docs/src/reference/types/filter/callback.md | 34 + .../types/filter/doctrine-orm/boolean.md | 21 + .../types/filter/doctrine-orm/date-range.md | 21 + .../types/filter/doctrine-orm/date-time.md | 21 + .../types/filter/doctrine-orm/date.md | 21 + .../types/filter/doctrine-orm/doctrine-orm.md | 16 + .../types/filter/doctrine-orm/entity.md | 76 + .../types/filter/doctrine-orm/numeric.md | 20 + .../types/filter/doctrine-orm/string.md | 20 + docs/src/reference/types/filter/filter.md | 11 + .../types/filter/options/doctrine-orm.md | 53 + .../reference/types/filter/options/filter.md | 90 + docs/src/reference/types/filter/search.md | 91 + docs/static/batch_action.png | Bin 20523 -> 0 bytes docs/static/favicon.png | Bin 4646 -> 0 bytes docs/static/global_action.png | Bin 3379 -> 0 bytes docs/static/row_actions.png | Bin 12346 -> 0 bytes docs/upgrade-guide/0.14.md | 188 -- docs/upgrade-guide/index.yml | 1 - phpstan.neon => phpstan.dist.neon | 3 + phpunit.xml.dist | 19 + src/AbstractExtension.php | 20 +- src/AbstractRegistry.php | 29 + src/Action/ActionRegistryInterface.php | 6 + .../Extension/PreloadedActionExtension.php | 4 +- .../Orm/Event/DoctrineOrmFilterEvent.php | 35 + .../Orm/Event/DoctrineOrmFilterEvents.php | 19 + .../Orm/Event/PreApplyExpressionEvent.php | 31 + .../Orm/Event/PreSetParametersEvent.php | 35 + .../ApplyExpressionTransformers.php | 44 + .../TransformDateRangeFilterData.php | 52 + .../Orm/Filter/DoctrineOrmFilterHandler.php | 65 + .../ExpressionFactory/ExpressionFactory.php | 56 + .../ExpressionFactoryInterface.php | 18 + ...bstractComparisonExpressionTransformer.php | 53 + .../CallbackExpressionTransformer.php | 20 + .../ExpressionTransformerInterface.php | 10 + .../LowerExpressionTransformer.php | 20 + .../TrimExpressionTransformer.php | 20 + .../UpperExpressionTransformer.php | 20 + .../Extension/DoctrineOrmFilterExtension.php | 32 + .../Formatter/DateActiveFilterFormatter.php | 22 + .../DateRangeActiveFilterFormatter.php | 33 + .../DateTimeActiveFilterFormatter.php | 38 + .../Formatter/EntityActiveFilterFormatter.php | 27 + .../ParameterFactory/ParameterFactory.php | 45 + .../ParameterFactoryInterface.php | 18 + .../Type/AbstractDoctrineOrmFilterType.php | 81 - .../Orm/Filter/Type/AbstractFilterType.php | 12 - .../Orm/Filter/Type/BooleanFilterType.php | 38 +- .../Orm/Filter/Type/CallbackFilterType.php | 3 + .../Orm/Filter/Type/DateFilterType.php | 92 +- .../Orm/Filter/Type/DateRangeFilterType.php | 86 +- .../Orm/Filter/Type/DateTimeFilterType.php | 112 +- .../Orm/Filter/Type/DoctrineOrmFilterType.php | 25 +- .../Orm/Filter/Type/EntityFilterType.php | 90 +- .../Orm/Filter/Type/NumericFilterType.php | 42 +- .../Orm/Filter/Type/StringFilterType.php | 50 +- .../Orm/Paginator/PaginatorFactory.php | 131 ++ .../Paginator/PaginatorFactoryInterface.php | 13 + .../Doctrine/Orm/Query/AliasResolver.php | 36 + .../Orm/Query/AliasResolverInterface.php | 12 + .../Orm/Query/DoctrineOrmProxyQuery.php | 167 +- .../Query/DoctrineOrmProxyQueryFactory.php | 3 +- .../Query/DoctrineOrmProxyQueryInterface.php | 27 +- .../Orm/Query/DoctrineOrmResultSetFactory.php | 62 + .../DoctrineOrmResultSetFactoryInterface.php | 13 + .../Test/ExpressionTransformerTestCase.php | 22 + .../Type/AbstractOpenSpoutExporterType.php | 4 +- src/Column/ColumnFactoryBuilder.php | 21 - src/Column/ColumnRegistryInterface.php | 6 + src/Column/ColumnSortUrlGenerator.php | 67 + .../ColumnSortUrlGeneratorInterface.php | 10 + .../Extension/PreloadedColumnExtension.php | 4 +- src/DataTable.php | 61 +- src/DataTableConfigBuilder.php | 8 +- src/DataTableConfigBuilderInterface.php | 2 +- src/DataTableConfigInterface.php | 2 +- src/DataTableRegistryInterface.php | 10 + .../KreyuDataTableExtension.php | 4 +- src/Exporter/ExporterRegistryInterface.php | 6 + .../Extension/PreloadedExporterExtension.php | 9 +- src/Extension/PreloadedDataTableExtension.php | 4 +- src/Filter/Event/FilterEvent.php | 40 + src/Filter/Event/FilterEvents.php | 16 + src/Filter/Event/PostHandleEvent.php | 9 + src/Filter/Event/PreHandleEvent.php | 9 + .../Extension/PreloadedFilterExtension.php | 9 +- src/Filter/Filter.php | 58 +- src/Filter/FilterClearUrlGenerator.php | 59 + .../FilterClearUrlGeneratorInterface.php | 10 + src/Filter/FilterConfigBuilder.php | 69 +- src/Filter/FilterConfigBuilderInterface.php | 18 +- src/Filter/FilterConfigInterface.php | 20 + src/Filter/FilterData.php | 24 +- src/Filter/FilterHandlerInterface.php | 12 + src/Filter/FilterInterface.php | 6 +- src/Filter/FilterRegistryInterface.php | 6 + src/Filter/FiltrationData.php | 30 +- src/Filter/Form/Type/FilterDataType.php | 77 +- src/Filter/Form/Type/FiltrationDataType.php | 48 +- src/Filter/Operator.php | 99 +- src/Filter/Type/CallbackFilterType.php | 33 + src/Filter/Type/FilterType.php | 165 +- src/Filter/Type/FilterTypeInterface.php | 3 - src/Filter/Type/ResolvedFilterType.php | 21 +- .../Type/ResolvedFilterTypeInterface.php | 3 - src/Filter/Type/SearchFilterType.php | 19 +- src/Pagination/Pagination.php | 19 +- src/Pagination/PaginationInterface.php | 5 - src/Persistence/CachePersistenceAdapter.php | 8 +- .../PersonalizationColumnData.php | 2 +- src/Personalization/PersonalizationData.php | 3 +- src/Query/ProxyQueryInterface.php | 5 +- src/Query/ResultSet.php | 35 + src/Query/ResultSetInterface.php | 12 + src/Resources/config/columns.php | 12 + src/Resources/config/filtration.php | 22 +- src/Resources/config/twig.php | 6 + .../translations/KreyuDataTable.pl.yaml | 3 + src/Resources/views/macros.html.twig | 27 - src/Resources/views/themes/base.html.twig | 6 +- .../views/themes/bootstrap_5.html.twig | 16 +- src/Sorting/SortingData.php | 1 + .../Exporter/ExporterIntegrationTestCase.php | 38 + src/Test/Exporter/ExporterTypeTestCase.php | 59 + src/Test/Filter/FilterIntegrationTestCase.php | 38 + src/Test/Filter/FilterTypeTestCase.php | 48 + src/Twig/DataTableExtension.php | 32 +- src/Type/DataTableType.php | 25 +- src/Type/ResolvedDataTableType.php | 12 +- src/Type/ResolvedDataTableTypeInterface.php | 7 + src/Util/RewindableGeneratorIterator.php | 51 + .../Fixtures/Filter/CustomFilterExtension.php | 32 + tests/Fixtures/Filter/CustomFilterType.php | 28 + .../Filter/CustomFilterTypeExtension.php | 41 + .../Filter/CustomParentFilterType.php | 11 + .../Orm/Event/PreSetParametersEventTest.php | 31 + .../DoctrineOrmFilterExtensionTest.php | 40 + .../Filter/DoctrineOrmFilterHandlerTest.php | 164 ++ .../ApplyExpressionTransformersTest.php | 92 + .../TransformDateRangeFilterDataTest.php | 85 + .../ExpressionFactoryTest.php | 202 +++ .../CallbackExpressionTransformerTest.php | 44 + .../ComparisonExpressionTransformerTest.php | 36 + .../LowerExpressionTransformerTest.php | 31 + .../TrimExpressionTransformerTest.php | 31 + .../UpperExpressionTransformerTest.php | 31 + .../DateActiveFilterFormatterTest.php | 50 + .../DateRangeActiveFilterFormatterTest.php | 65 + .../DateTimeActiveFilterFormatterTest.php | 60 + .../EntityActiveFilterFormatterTest.php | 46 + .../ParameterFactory/ParameterFactoryTest.php | 162 ++ .../Orm/Filter/Type/BooleanFilterTypeTest.php | 78 + .../Orm/Filter/Type/DateFilterTypeTest.php | 64 + .../Filter/Type/DateRangeFilterTypeTest.php | 34 + .../Filter/Type/DateTimeFilterTypeTest.php | 64 + .../Type/DoctrineOrmFilterTypeTestCase.php | 34 + .../Orm/Filter/Type/EntityFilterTypeTest.php | 124 ++ .../Orm/Filter/Type/NumericFilterTypeTest.php | 34 + .../Orm/Filter/Type/StringFilterTypeTest.php | 33 + .../Doctrine/Orm/Fixtures/Entity/Car.php | 29 + .../Doctrine/Orm/Fixtures/Entity/Category.php | 29 + .../Doctrine/Orm/Fixtures/Entity/Product.php | 36 + .../Orm/Fixtures/Entity/ProductAttribute.php | 29 + .../CustomExpressionTransformer.php | 20 + .../Fixtures/Query/NotSupportedProxyQuery.php | 40 + .../Orm/Fixtures/TestEntityManagerFactory.php | 35 + .../Orm/Paginator/PaginatorFactoryTest.php | 154 ++ .../Doctrine/Orm/Query/AliasResolverTest.php | 68 + .../DoctrineOrmProxyQueryFactoryTest.php | 41 + .../Orm/Query/DoctrineOrmProxyQueryTest.php | 286 +++ .../Query/DoctrineOrmResultSetFactoryTest.php | 87 + .../Column/ColumnSortUrlGeneratorTest.php | 106 ++ .../Filter/FilterClearUrlGeneratorTest.php | 115 ++ .../Unit/Filter/FilterFactoryBuilderTest.php | 78 + tests/Unit/Filter/FilterFactoryTest.php | 53 + tests/Unit/Filter/FilterRegistryTest.php | 116 ++ tests/Unit/Filter/FilterTest.php | 130 ++ .../Filter/Type/CallbackFilterTypeTest.php | 37 + tests/Unit/Filter/Type/FilterTypeTest.php | 314 ++++ .../CachePersistenceAdapterTest.php | 42 + 332 files changed, 11980 insertions(+), 6790 deletions(-) delete mode 100755 .github/workflows/retype-action.yml create mode 100644 .github/workflows/testing.yml create mode 100644 docs/.gitignore create mode 100644 docs/.vitepress/config.mts create mode 100644 docs/.vitepress/theme/index.js create mode 100644 docs/.vitepress/theme/style.css create mode 100644 docs/README.md delete mode 100755 docs/basic-usage/adding-actions.md delete mode 100755 docs/basic-usage/adding-columns.md delete mode 100755 docs/basic-usage/creating-data-tables.md delete mode 100755 docs/basic-usage/defining-the-filters.md delete mode 100755 docs/basic-usage/disclaimer.md delete mode 100755 docs/basic-usage/enabling-global-search.md delete mode 100755 docs/basic-usage/enabling-persistence.md delete mode 100755 docs/basic-usage/exporting-the-data.md delete mode 100755 docs/basic-usage/index.yml delete mode 100755 docs/basic-usage/internationalization.md delete mode 100755 docs/basic-usage/persisting-applied-data.md delete mode 100755 docs/basic-usage/rendering-the-table.md delete mode 100755 docs/basic-usage/summary.md delete mode 100644 docs/contributing.md delete mode 100755 docs/features/actions/actions.md delete mode 100755 docs/features/actions/batch-actions.md delete mode 100755 docs/features/actions/global-actions.md delete mode 100644 docs/features/actions/index.yml delete mode 100755 docs/features/actions/row-actions.md delete mode 100644 docs/features/columns.md delete mode 100755 docs/features/filtering.md delete mode 100755 docs/features/global-search.md delete mode 100755 docs/features/index.yml delete mode 100755 docs/features/proxy-queries.md delete mode 100755 docs/features/request-handlers.md delete mode 100755 docs/features/type-classes.md delete mode 100755 docs/index.md create mode 100644 docs/package-lock.json create mode 100644 docs/package.json delete mode 100755 docs/reference/actions/index.yml delete mode 100755 docs/reference/actions/types.md delete mode 100755 docs/reference/actions/types/action.md delete mode 100755 docs/reference/actions/types/link.md delete mode 100755 docs/reference/columns/index.yml delete mode 100755 docs/reference/columns/types.md delete mode 100755 docs/reference/columns/types/actions.md delete mode 100755 docs/reference/columns/types/boolean.md delete mode 100755 docs/reference/columns/types/checkbox.md delete mode 100755 docs/reference/columns/types/column.md delete mode 100755 docs/reference/columns/types/date-period.md delete mode 100755 docs/reference/columns/types/date-time.md delete mode 100644 docs/reference/columns/types/date.md delete mode 100755 docs/reference/columns/types/form.md delete mode 100755 docs/reference/columns/types/template.md delete mode 100755 docs/reference/columns/types/text.md delete mode 100755 docs/reference/exporters/index.yml delete mode 100755 docs/reference/exporters/types.md delete mode 100755 docs/reference/exporters/types/exporter.md delete mode 100644 docs/reference/exporters/types/open-spout/_open-spout_options.md delete mode 100755 docs/reference/exporters/types/open-spout/csv.md delete mode 100755 docs/reference/exporters/types/open-spout/index.yml delete mode 100755 docs/reference/exporters/types/open-spout/ods.md delete mode 100644 docs/reference/exporters/types/open-spout/open-spout.md delete mode 100755 docs/reference/exporters/types/php-spreadsheet/html.md delete mode 100755 docs/reference/exporters/types/php-spreadsheet/index.yml delete mode 100755 docs/reference/exporters/types/php-spreadsheet/ods.md delete mode 100755 docs/reference/exporters/types/php-spreadsheet/php-spreadsheet.md delete mode 100755 docs/reference/exporters/types/php-spreadsheet/xls.md delete mode 100755 docs/reference/exporters/types/php-spreadsheet/xlsx.md delete mode 100755 docs/reference/filters/index.yml delete mode 100755 docs/reference/filters/types.md delete mode 100755 docs/reference/filters/types/_filter_options.md delete mode 100644 docs/reference/filters/types/doctrine-orm/_doctrine_orm_filter_options.md delete mode 100755 docs/reference/filters/types/doctrine-orm/boolean.md delete mode 100755 docs/reference/filters/types/doctrine-orm/callback.md delete mode 100755 docs/reference/filters/types/doctrine-orm/date-time.md delete mode 100755 docs/reference/filters/types/doctrine-orm/date.md delete mode 100644 docs/reference/filters/types/doctrine-orm/doctrine-orm.md delete mode 100755 docs/reference/filters/types/doctrine-orm/entity.md delete mode 100755 docs/reference/filters/types/doctrine-orm/index.yml delete mode 100755 docs/reference/filters/types/doctrine-orm/numeric.md delete mode 100755 docs/reference/filters/types/doctrine-orm/string.md delete mode 100755 docs/reference/filters/types/filter.md delete mode 100644 docs/reference/filters/types/search.md delete mode 100755 docs/reference/index.yml delete mode 100755 docs/retype.yml create mode 100644 docs/src/docs/components/actions.md create mode 100644 docs/src/docs/components/columns.md create mode 100644 docs/src/docs/components/exporters.md create mode 100644 docs/src/docs/components/filters.md create mode 100644 docs/src/docs/contributing.md rename docs/{features/symfony-ux-turbo.md => src/docs/features/asynchronicity.md} (57%) mode change 100755 => 100644 rename docs/{ => src/docs}/features/exporting.md (70%) mode change 100755 => 100644 create mode 100644 docs/src/docs/features/extensibility.md create mode 100644 docs/src/docs/features/filtering.md rename docs/{ => src/docs}/features/pagination.md (74%) mode change 100755 => 100644 rename docs/{ => src/docs}/features/persistence.md (61%) mode change 100755 => 100644 rename docs/{ => src/docs}/features/personalization.md (74%) mode change 100755 => 100644 rename docs/{ => src/docs}/features/sorting.md (79%) mode change 100755 => 100644 rename docs/{ => src/docs}/features/theming.md (71%) mode change 100755 => 100644 rename docs/{ => src/docs}/installation.md (54%) mode change 100755 => 100644 create mode 100644 docs/src/docs/integrations/doctrine-orm/events.md create mode 100644 docs/src/docs/integrations/doctrine-orm/expression-transformers.md create mode 100644 docs/src/docs/introduction.md rename docs/{ => src/docs}/troubleshooting.md (78%) mode change 100755 => 100644 create mode 100644 docs/src/index.md rename docs/{static => src/public}/action_confirmation_modal.png (100%) mode change 100755 => 100644 rename docs/{static => src/public}/export_modal.png (100%) mode change 100755 => 100644 create mode 100644 docs/src/public/logo.png rename docs/{static => src/public}/personalization_modal.png (100%) mode change 100755 => 100644 rename docs/{static/global_search.png => src/public/search_filter_type.png} (100%) mode change 100755 => 100644 rename docs/{ => src}/reference/configuration.md (85%) mode change 100755 => 100644 rename docs/{ => src}/reference/twig.md (92%) mode change 100755 => 100644 create mode 100644 docs/src/reference/types/action.md create mode 100644 docs/src/reference/types/action/action.md rename docs/{reference/actions/types => src/reference/types/action}/button.md (53%) mode change 100755 => 100644 rename docs/{reference/actions/types => src/reference/types/action}/form.md (57%) mode change 100755 => 100644 create mode 100644 docs/src/reference/types/action/link.md rename docs/{reference/actions/types/_action_options.md => src/reference/types/action/options/action.md} (58%) mode change 100755 => 100644 create mode 100644 docs/src/reference/types/column.md create mode 100644 docs/src/reference/types/column/actions.md create mode 100644 docs/src/reference/types/column/boolean.md create mode 100644 docs/src/reference/types/column/checkbox.md rename docs/{reference/columns/types => src/reference/types/column}/collection.md (60%) mode change 100755 => 100644 create mode 100644 docs/src/reference/types/column/column.md create mode 100644 docs/src/reference/types/column/date-period.md create mode 100644 docs/src/reference/types/column/date-time.md create mode 100644 docs/src/reference/types/column/date.md rename docs/{reference/columns/types => src/reference/types/column}/link.md (56%) mode change 100755 => 100644 rename docs/{reference/columns/types => src/reference/types/column}/money.md (79%) mode change 100755 => 100644 rename docs/{reference/columns/types => src/reference/types/column}/number.md (66%) mode change 100755 => 100644 rename docs/{reference/columns/types/_column_options.md => src/reference/types/column/options/column.md} (81%) mode change 100755 => 100644 create mode 100644 docs/src/reference/types/column/template.md create mode 100644 docs/src/reference/types/column/text.md create mode 100644 docs/src/reference/types/data-table.md create mode 100644 docs/src/reference/types/exporter.md create mode 100644 docs/src/reference/types/exporter/callback.md create mode 100644 docs/src/reference/types/exporter/exporter.md create mode 100644 docs/src/reference/types/exporter/open-spout/csv.md create mode 100644 docs/src/reference/types/exporter/open-spout/ods.md rename docs/{reference/exporters/types => src/reference/types/exporter}/open-spout/xlsx.md (57%) mode change 100755 => 100644 rename docs/{reference/exporters/types/_exporter_options.md => src/reference/types/exporter/options/exporter.md} (88%) mode change 100755 => 100644 rename docs/{reference/exporters/types/php-spreadsheet/_php-spreadsheet_options.md => src/reference/types/exporter/options/php-spreadsheet.md} (83%) mode change 100755 => 100644 rename docs/{reference/exporters/types => src/reference/types/exporter}/php-spreadsheet/csv.md (77%) mode change 100755 => 100644 rename docs/{reference/exporters/types/php-spreadsheet/_html_options.md => src/reference/types/exporter/php-spreadsheet/html.md} (77%) mode change 100755 => 100644 create mode 100644 docs/src/reference/types/exporter/php-spreadsheet/ods.md create mode 100644 docs/src/reference/types/exporter/php-spreadsheet/pdf.md create mode 100644 docs/src/reference/types/exporter/php-spreadsheet/xls.md create mode 100644 docs/src/reference/types/exporter/php-spreadsheet/xlsx.md create mode 100644 docs/src/reference/types/filter.md create mode 100644 docs/src/reference/types/filter/callback.md create mode 100644 docs/src/reference/types/filter/doctrine-orm/boolean.md create mode 100644 docs/src/reference/types/filter/doctrine-orm/date-range.md create mode 100644 docs/src/reference/types/filter/doctrine-orm/date-time.md create mode 100644 docs/src/reference/types/filter/doctrine-orm/date.md create mode 100644 docs/src/reference/types/filter/doctrine-orm/doctrine-orm.md create mode 100644 docs/src/reference/types/filter/doctrine-orm/entity.md create mode 100644 docs/src/reference/types/filter/doctrine-orm/numeric.md create mode 100644 docs/src/reference/types/filter/doctrine-orm/string.md create mode 100644 docs/src/reference/types/filter/filter.md create mode 100644 docs/src/reference/types/filter/options/doctrine-orm.md create mode 100644 docs/src/reference/types/filter/options/filter.md create mode 100644 docs/src/reference/types/filter/search.md delete mode 100755 docs/static/batch_action.png delete mode 100755 docs/static/favicon.png delete mode 100755 docs/static/global_action.png delete mode 100755 docs/static/row_actions.png delete mode 100755 docs/upgrade-guide/0.14.md delete mode 100755 docs/upgrade-guide/index.yml rename phpstan.neon => phpstan.dist.neon (83%) create mode 100644 phpunit.xml.dist create mode 100644 src/Bridge/Doctrine/Orm/Event/DoctrineOrmFilterEvent.php create mode 100644 src/Bridge/Doctrine/Orm/Event/DoctrineOrmFilterEvents.php create mode 100644 src/Bridge/Doctrine/Orm/Event/PreApplyExpressionEvent.php create mode 100644 src/Bridge/Doctrine/Orm/Event/PreSetParametersEvent.php create mode 100644 src/Bridge/Doctrine/Orm/EventListener/ApplyExpressionTransformers.php create mode 100644 src/Bridge/Doctrine/Orm/EventListener/TransformDateRangeFilterData.php create mode 100644 src/Bridge/Doctrine/Orm/Filter/DoctrineOrmFilterHandler.php create mode 100644 src/Bridge/Doctrine/Orm/Filter/ExpressionFactory/ExpressionFactory.php create mode 100644 src/Bridge/Doctrine/Orm/Filter/ExpressionFactory/ExpressionFactoryInterface.php create mode 100644 src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/AbstractComparisonExpressionTransformer.php create mode 100644 src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/CallbackExpressionTransformer.php create mode 100644 src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/ExpressionTransformerInterface.php create mode 100644 src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/LowerExpressionTransformer.php create mode 100644 src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/TrimExpressionTransformer.php create mode 100644 src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/UpperExpressionTransformer.php create mode 100644 src/Bridge/Doctrine/Orm/Filter/Extension/DoctrineOrmFilterExtension.php create mode 100644 src/Bridge/Doctrine/Orm/Filter/Formatter/DateActiveFilterFormatter.php create mode 100644 src/Bridge/Doctrine/Orm/Filter/Formatter/DateRangeActiveFilterFormatter.php create mode 100644 src/Bridge/Doctrine/Orm/Filter/Formatter/DateTimeActiveFilterFormatter.php create mode 100644 src/Bridge/Doctrine/Orm/Filter/Formatter/EntityActiveFilterFormatter.php create mode 100644 src/Bridge/Doctrine/Orm/Filter/ParameterFactory/ParameterFactory.php create mode 100644 src/Bridge/Doctrine/Orm/Filter/ParameterFactory/ParameterFactoryInterface.php delete mode 100755 src/Bridge/Doctrine/Orm/Filter/Type/AbstractFilterType.php mode change 100755 => 100644 src/Bridge/Doctrine/Orm/Filter/Type/BooleanFilterType.php mode change 100755 => 100644 src/Bridge/Doctrine/Orm/Filter/Type/CallbackFilterType.php mode change 100755 => 100644 src/Bridge/Doctrine/Orm/Filter/Type/DateFilterType.php mode change 100755 => 100644 src/Bridge/Doctrine/Orm/Filter/Type/DateRangeFilterType.php mode change 100755 => 100644 src/Bridge/Doctrine/Orm/Filter/Type/DateTimeFilterType.php mode change 100755 => 100644 src/Bridge/Doctrine/Orm/Filter/Type/DoctrineOrmFilterType.php mode change 100755 => 100644 src/Bridge/Doctrine/Orm/Filter/Type/EntityFilterType.php mode change 100755 => 100644 src/Bridge/Doctrine/Orm/Filter/Type/NumericFilterType.php mode change 100755 => 100644 src/Bridge/Doctrine/Orm/Filter/Type/StringFilterType.php create mode 100644 src/Bridge/Doctrine/Orm/Paginator/PaginatorFactory.php create mode 100644 src/Bridge/Doctrine/Orm/Paginator/PaginatorFactoryInterface.php create mode 100644 src/Bridge/Doctrine/Orm/Query/AliasResolver.php create mode 100644 src/Bridge/Doctrine/Orm/Query/AliasResolverInterface.php mode change 100755 => 100644 src/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQuery.php mode change 100755 => 100644 src/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryFactory.php create mode 100644 src/Bridge/Doctrine/Orm/Query/DoctrineOrmResultSetFactory.php create mode 100644 src/Bridge/Doctrine/Orm/Query/DoctrineOrmResultSetFactoryInterface.php create mode 100644 src/Bridge/Doctrine/Orm/Test/ExpressionTransformerTestCase.php create mode 100644 src/Column/ColumnSortUrlGenerator.php create mode 100644 src/Column/ColumnSortUrlGeneratorInterface.php create mode 100644 src/Filter/Event/FilterEvent.php create mode 100644 src/Filter/Event/FilterEvents.php create mode 100644 src/Filter/Event/PostHandleEvent.php create mode 100644 src/Filter/Event/PreHandleEvent.php create mode 100644 src/Filter/FilterClearUrlGenerator.php create mode 100644 src/Filter/FilterClearUrlGeneratorInterface.php create mode 100644 src/Filter/FilterHandlerInterface.php create mode 100644 src/Filter/Type/CallbackFilterType.php create mode 100644 src/Query/ResultSet.php create mode 100644 src/Query/ResultSetInterface.php delete mode 100755 src/Resources/views/macros.html.twig create mode 100644 src/Test/Exporter/ExporterIntegrationTestCase.php create mode 100644 src/Test/Exporter/ExporterTypeTestCase.php create mode 100644 src/Test/Filter/FilterIntegrationTestCase.php create mode 100644 src/Test/Filter/FilterTypeTestCase.php create mode 100644 src/Util/RewindableGeneratorIterator.php create mode 100644 tests/Fixtures/Filter/CustomFilterExtension.php create mode 100644 tests/Fixtures/Filter/CustomFilterType.php create mode 100644 tests/Fixtures/Filter/CustomFilterTypeExtension.php create mode 100644 tests/Fixtures/Filter/CustomParentFilterType.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Event/PreSetParametersEventTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Extension/DoctrineOrmFilterExtensionTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/DoctrineOrmFilterHandlerTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/EventListener/ApplyExpressionTransformersTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/EventListener/TransformDateRangeFilterDataTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionFactory/ExpressionFactoryTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/CallbackExpressionTransformerTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/ComparisonExpressionTransformerTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/LowerExpressionTransformerTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/TrimExpressionTransformerTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/UpperExpressionTransformerTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/Formatter/DateActiveFilterFormatterTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/Formatter/DateRangeActiveFilterFormatterTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/Formatter/DateTimeActiveFilterFormatterTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/Formatter/EntityActiveFilterFormatterTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/ParameterFactory/ParameterFactoryTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/Type/BooleanFilterTypeTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/Type/DateFilterTypeTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/Type/DateRangeFilterTypeTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/Type/DateTimeFilterTypeTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/Type/DoctrineOrmFilterTypeTestCase.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/Type/EntityFilterTypeTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/Type/NumericFilterTypeTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Filter/Type/StringFilterTypeTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Fixtures/Entity/Car.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Fixtures/Entity/Category.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Fixtures/Entity/Product.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Fixtures/Entity/ProductAttribute.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Fixtures/Filter/ExpressionTransformer/CustomExpressionTransformer.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Fixtures/Query/NotSupportedProxyQuery.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Fixtures/TestEntityManagerFactory.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Paginator/PaginatorFactoryTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Query/AliasResolverTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryFactoryTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryTest.php create mode 100644 tests/Unit/Bridge/Doctrine/Orm/Query/DoctrineOrmResultSetFactoryTest.php create mode 100644 tests/Unit/Column/ColumnSortUrlGeneratorTest.php create mode 100644 tests/Unit/Filter/FilterClearUrlGeneratorTest.php create mode 100644 tests/Unit/Filter/FilterFactoryBuilderTest.php create mode 100644 tests/Unit/Filter/FilterFactoryTest.php create mode 100644 tests/Unit/Filter/FilterRegistryTest.php create mode 100644 tests/Unit/Filter/FilterTest.php create mode 100644 tests/Unit/Filter/Type/CallbackFilterTypeTest.php create mode 100644 tests/Unit/Filter/Type/FilterTypeTest.php create mode 100644 tests/Unit/Persistence/CachePersistenceAdapterTest.php diff --git a/.github/workflows/retype-action.yml b/.github/workflows/retype-action.yml deleted file mode 100755 index 4519df02..00000000 --- a/.github/workflows/retype-action.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Documentation -on: - workflow_dispatch: - push: - branches: - - main -jobs: - publish: - name: Publish documentation - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v2 - - uses: retypeapp/action-build@latest - - uses: retypeapp/action-github-pages@latest - with: - update-branch: true \ No newline at end of file diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 00000000..6081f18d --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,20 @@ +name: Testing + +on: [push] + +jobs: + build-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: php-actions/composer@v6 + with: + php_extensions: dom fileinfo filter libxml xmlreader zip gd + - uses: php-actions/phpstan@v3 + - uses: php-actions/phpunit@v3 + with: + php_extensions: xdebug dom fileinfo filter libxml xmlreader zip gd + args: --coverage-text + env: + XDEBUG_MODE: coverage diff --git a/.gitignore b/.gitignore index 33c9215b..9d5238c4 100755 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /venv /composer.lock /.php-cs-fixer.cache +/.phpunit.cache /.phpunit.result.cache /.idea /.vscode \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 44b7815a..04fc4ab9 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# 0.16 + +- **[Feature]** French translation (https://github.com/Kreyu/data-table-bundle/pull/53) +- **[Feature]** Doctrine ORM expression transformers with built-in `lower`, `upper` and `trim` options (https://github.com/Kreyu/data-table-bundle/issues/50) +- **[Feature]** Filter handler events +- **[Docs]** New documentation, written using [VitePress](https://vitepress.dev/) + +# 0.15 + +- **[Feature]** Integration with AssetMapper (https://github.com/Kreyu/data-table-bundle/issues/42) + # 0.14 - **[Feature]** Data table events @@ -22,7 +33,7 @@ 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). +For a list of all breaking changes and deprecations, see the [upgrade guide](../../../docs/upgrade-guide/0.14.md). # 0.13 diff --git a/composer.json b/composer.json index c0ad0eba..2c3bf0eb 100755 --- a/composer.json +++ b/composer.json @@ -31,11 +31,19 @@ "phpoffice/phpspreadsheet": "^1.28", "doctrine/orm": "^2.15", "doctrine/doctrine-bundle": "^2.9", - "phpstan/phpstan": "^1.10" + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.4", + "dg/bypass-finals": "dev-master", + "openspout/openspout": "^4.23" }, "autoload": { "psr-4": { "Kreyu\\Bundle\\DataTableBundle\\": "src" } + }, + "autoload-dev": { + "psr-4": { + "Kreyu\\Bundle\\DataTableBundle\\Tests\\": "tests" + } } } diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..39b34360 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.vitepress/cache/ +.vitepress/dist/ \ No newline at end of file diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts new file mode 100644 index 00000000..6c3ea24d --- /dev/null +++ b/docs/.vitepress/config.mts @@ -0,0 +1,175 @@ +import { defineConfig } from 'vitepress' + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: 'DataTableBundle', + description: 'Streamlines creation process of the data tables', + srcDir: './src', + head: [ + ['link', { rel: 'icon', type: 'image/png', href: '/logo.png' }], + ], + themeConfig: { + logo: '/logo.png', + externalLinkIcon: true, + outline: 'deep', + + // https://vitepress.dev/reference/default-theme-config + nav: [ + { text: 'Home', link: '/' }, + { text: 'Documentation', link: '/docs/introduction', activeMatch: '/docs/' }, + { text: 'Reference', link: '/reference/types/data-table', activeMatch: '/reference/' }, + ], + + sidebar: { + '/docs/': [ + { + text: 'Getting started', + items: [ + { text: 'Introduction', link: '/docs/introduction' }, + { text: 'Installation', link: '/docs/installation' }, + ] + }, + { + text: 'Components', + items: [ + { text: 'Columns', link: '/docs/components/columns' }, + { text: 'Filters', link: '/docs/components/filters' }, + { text: 'Actions', link: '/docs/components/actions' }, + { text: 'Exporters', link: '/docs/components/exporters' }, + ] + }, + { + text: 'Features', + items: [ + { text: 'Sorting', link: '/docs/features/sorting' }, + { text: 'Filtering', link: '/docs/features/filtering' }, + { text: 'Exporting', link: '/docs/features/exporting' }, + { text: 'Pagination', link: '/docs/features/pagination' }, + { text: 'Personalization', link: '/docs/features/personalization' }, + { text: 'Persistence', link: '/docs/features/persistence' }, + { text: 'Theming', link: '/docs/features/theming' }, + { text: 'Asynchronicity', link: '/docs/features/asynchronicity' }, + { text: 'Extensibility', link: '/docs/features/extensibility' }, + ] + }, + { + text: 'Integrations', + items: [ + { + text: 'Doctrine ORM', + collapsed: true, + items: [ + { text: 'Expression transformers', link: '/docs/integrations/doctrine-orm/expression-transformers' }, + { text: 'Events', link: '/docs/integrations/doctrine-orm/events' } + ], + }, + ] + }, + { text: 'Troubleshooting', link: '/docs/troubleshooting' }, + { text: 'Contributing', link: '/docs/contributing' }, + ], + '/reference/': [ + { + text: 'Types', + items: [ + { + text: 'DataTable', + link: '/reference/types/data-table' + }, + { + text: 'Column', + link: '/reference/types/column', + collapsed: true, + items: [ + { text: 'Text', link: '/reference/types/column/text' }, + { text: 'Number', link: '/reference/types/column/number' }, + { text: 'Money', link: '/reference/types/column/money' }, + { text: 'Boolean', link: '/reference/types/column/boolean' }, + { text: 'Link', link: '/reference/types/column/link' }, + { text: 'Date', link: '/reference/types/column/date' }, + { text: 'DateTime', link: '/reference/types/column/date-time' }, + { text: 'DatePeriod', link: '/reference/types/column/date-period' }, + { text: 'Collection', link: '/reference/types/column/collection' }, + { text: 'Template', link: '/reference/types/column/template' }, + { text: 'Actions', link: '/reference/types/column/actions' }, + { text: 'Checkbox', link: '/reference/types/column/checkbox' }, + { text: 'Column', link: '/reference/types/column/column' }, + ] + }, + { + text: 'Filter', + link: '/reference/types/filter', + collapsed: true, + items: [ + { + text: 'Doctrine ORM', + collapsed: false, + items: [ + { text: 'String', link: '/reference/types/filter/doctrine-orm/string' }, + { text: 'Numeric', link: '/reference/types/filter/doctrine-orm/numeric' }, + { text: 'Boolean', link: '/reference/types/filter/doctrine-orm/boolean' }, + { text: 'Date', link: '/reference/types/filter/doctrine-orm/date' }, + { text: 'DateTime', link: '/reference/types/filter/doctrine-orm/date-time' }, + { text: 'DateRange', link: '/reference/types/filter/doctrine-orm/date-range' }, + { text: 'Entity', link: '/reference/types/filter/doctrine-orm/entity' }, + { text: 'DoctrineOrm', link: '/reference/types/filter/doctrine-orm/doctrine-orm' }, + ], + }, + { text: 'Callback', link: '/reference/types/filter/callback' }, + { text: 'Search', link: '/reference/types/filter/search' }, + { text: 'Filter', link: '/reference/types/filter/filter' }, + ], + }, + { + text: 'Action', + link: '/reference/types/action', + collapsed: true, + items: [ + { text: 'Link', link: '/reference/types/action/link' }, + { text: 'Button', link: '/reference/types/action/button' }, + { text: 'Form', link: '/reference/types/action/form' }, + { text: 'Action', link: '/reference/types/action/action' }, + ], + }, + { + text: 'Exporter', + link: '/reference/types/exporter', + collapsed: true, + items: [ + { + text: 'PhpSpreadsheet', + collapsed: false, + items: [ + { text: 'Csv', link: '/reference/types/exporter/php-spreadsheet/csv' }, + { text: 'Xls', link: '/reference/types/exporter/php-spreadsheet/xls' }, + { text: 'Xlsx', link: '/reference/types/exporter/php-spreadsheet/xlsx' }, + { text: 'Ods', link: '/reference/types/exporter/php-spreadsheet/ods' }, + { text: 'Pdf', link: '/reference/types/exporter/php-spreadsheet/pdf' }, + { text: 'Html', link: '/reference/types/exporter/php-spreadsheet/html' }, + ], + }, + { + text: 'OpenSpout', + collapsed: false, + items: [ + { text: 'Csv', link: '/reference/types/exporter/open-spout/csv' }, + { text: 'Xlsx', link: '/reference/types/exporter/open-spout/xlsx' }, + { text: 'Ods', link: '/reference/types/exporter/open-spout/ods' }, + ], + }, + { text: 'Callback', link: '/reference/types/exporter/callback' }, + { text: 'Exporter', link: '/reference/types/exporter/exporter' }, + ], + }, + ] + }, + { text: 'Configuration', link: '/reference/configuration' }, + { text: 'Twig', link: '/reference/twig' }, + ], + }, + + socialLinks: [ + { icon: 'github', link: 'https://github.com/kreyu/data-table-bundle' } + ] + } +}) diff --git a/docs/.vitepress/theme/index.js b/docs/.vitepress/theme/index.js new file mode 100644 index 00000000..0ed10c5f --- /dev/null +++ b/docs/.vitepress/theme/index.js @@ -0,0 +1,4 @@ +import DefaultTheme from 'vitepress/theme' +import './style.css' + +export default DefaultTheme \ No newline at end of file diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css new file mode 100644 index 00000000..da2d41d5 --- /dev/null +++ b/docs/.vitepress/theme/style.css @@ -0,0 +1,30 @@ +.VPContent.is-home { + display: flex; + align-items: center; + justify-content: center; +} + +:root { + --vp-button-brand-bg: #7986CB; + --vp-button-brand-hover-bg: #9FA8DA; + + --vp-c-brand-1: #7986CB; + --vp-c-brand-2: #9FA8DA; + + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: -webkit-linear-gradient(120deg, #32bffc 10%, #7986CB); + + --vp-home-hero-image-background-image: -webkit-linear-gradient(120deg, rgba(68, 113, 210, 1), 30%, rgba(50, 191, 252, 1)); +} + +@media (min-width: 640px) { + :root { + --vp-home-hero-image-filter: blur(56px); + } +} + +@media (min-width: 960px) { + :root { + --vp-home-hero-image-filter: blur(65px); + } +} \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..e92d3786 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,7 @@ +# Documentation + +To start the development server, run following commands: + +```shell +npm install && npm run docs:dev +``` diff --git a/docs/basic-usage/adding-actions.md b/docs/basic-usage/adding-actions.md deleted file mode 100755 index 7c1ba7ab..00000000 --- a/docs/basic-usage/adding-actions.md +++ /dev/null @@ -1,207 +0,0 @@ ---- -order: h ---- - -# Adding actions - -What is a list of products without the ability of creating and editing the records? -In this bundle, there are _three_ kinds of actions: - -* [global actions](#adding-global-actions), displayed above the data table - e.g. "Create new user"; -* [batch actions](#adding-batch-actions), requiring the user to select one or more rows, e.g. "Delete selected"; -* [row actions](#adding-row-actions), displayed on each row, e.g. "Edit", "Delete"; - -Similar to data tables, columns and filters, actions are defined using the [type classes](../features/type-classes.md). - -## Adding global actions - -![Global action with the Tabler theme](../static/global_action.png)-- - -Let's assume that the application has an `app_product_create` route for creating new products. -The user should be able to click a "Create new product" button above the data table. - -To add global action, use the builder's `addAction()` method: - -```php # src/DataTable/Type/ProductDataTableType.php -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; -use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - // Columns, filters and exporters added before... - - $builder - ->addAction('create', ButtonActionType::class, [ - 'label' => 'Create new product', - 'href' => $this->urlGenerator->generate('app_product_create'), - 'icon_attr' => [ - // For example, using Tabler Icons - // https://tabler-icons.io/ - 'class' => 'ti ti-plus', - ], - ]) - ; - } -} -``` - -The builder's `addAction()` method accepts _three_ arguments: - -- action name; -- action type — with a fully qualified class name; -- action options — defined by the action type, used to configure the action; - -For reference, see [built-in action types](../components/actions/types.md). - -## Adding batch actions - -![Batch action with the Tabler theme](../../static/batch_action.png)-- - -Let's assume that the application has an `app_product_delete_batch` route for deleting products. -The user should be able to select a few rows and click a "Delete selected" button above the data table. - -To begin with, make sure the [Symfony UX integration is enabled](../installation.md#enable-the-symfony-ux-integration). -Then, enable the `batch` controller: - -```json # assets/controllers.json -{ - "controllers": { - "@kreyu/data-table-bundle": { - "batch": { - "enabled": true - } - } - } -} -``` - -To add batch action, use the builder's `addBatchAction()` method: - -```php # src/DataTable/Type/ProductDataTableType.php -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; -use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - // Columns, filters and exporters added before... - - $builder - ->addBatchAction('delete', FormActionType::class, [ - 'label' => 'Delete selected', - 'action' => $this->urlGenerator->generate('app_product_delete_batch'), - 'method' => 'DELETE', - 'icon_attr' => [ - // For example, using Tabler Icons - // https://tabler-icons.io/ - 'class' => 'ti ti-trash', - ], - ]) - ; - } -} -``` - -Similarly to the builder's `addAction()` method, the `addBatchAction()` method accepts _three_ arguments: - -- action name; -- action type — with a fully qualified class name; -- action options — defined by the action type, used to configure the action; - -For reference, see [built-in action types](../components/actions/types.md). - -## Adding row actions - -![Row actions with the built-in Tabler theme](../static/row_actions.png)-- - -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 row action, use the builder's `addRowAction()` method: - -```php # src/DataTable/Type/ProductDataTableType.php -use App\Entity\Product; -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; -use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; -use Kreyu\Bundle\DataTableBundle\Column\Type\ActionsColumnType; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - // Columns, filters and exporters added before... - - $builder - ->addRowAction('show', ButtonActionType::class, [ - 'label' => false, - 'href' => function (Product $product): string { - return $this->urlGenerator->generate('app_product_show', [ - 'id' => $product->getId(), - ]); - }, - 'icon_attr' => [ - // For example, using Tabler Icons - // https://tabler-icons.io/ - 'class' => 'ti ti-user icon', - ], - ]) - ; - } -} -``` - -Similarly to the builder's `addAction()` and `addBatchAction()` methods, the `addRowAction()` method accepts _three_ arguments: - -- action name; -- action type — with a fully qualified class name; -- action options — defined by the action type, used to configure the action; - -For reference, see [built-in action types](../components/actions/types.md). - -## Enabling action confirmation - -Clicking on the delete action immediately removes the products — in some cases it may be fine, but dangerous actions should be confirmable by the user. - -![Action confirmation modal with the Tabler theme](../static/action_confirmation_modal.png) - -By default, actions are **not** confirmable, because their `confirmation` option equals `false`. To change that, set the option to `true`: - -```php # src/DataTable/Type/ProductDataTableType.php -use App\Entity\Product; -use Kreyu\Bundle\DataTableBundle\Action\Type\FormActionType; -use Kreyu\Bundle\DataTableBundle\Column\Type\ActionsColumnType; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - // Columns, filters and exporters added before... - - $builder - ->addColumn('actions', ActionsColumnType::class, [ - 'actions' => [ - 'update' => [ - // Configuration added before... - ], - 'delete' => [ - 'type' => FormActionType::class, - 'type_options' => [ - // Other action type options defined before... - 'confirmation' => true, - ], - ], - ], - ]) - ; - } -} -``` - -Now that the data table seems to be complete, let's focus on something really special — [a personalization](../basic-usage/enabling-persistence.md), where the user can decide which columns are visible, or even their order! diff --git a/docs/basic-usage/adding-columns.md b/docs/basic-usage/adding-columns.md deleted file mode 100755 index 87766d4f..00000000 --- a/docs/basic-usage/adding-columns.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -order: c ---- - -# Adding columns - -The data table builder object can be used to describe the columns used in the table. -Similar to data tables, the columns are defined using the [type classes](../features/type-classes.md). - -## Adding columns to the data table - -Let's start by adding a column for each field in the product entity: - -```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 - { - $builder - ->addColumn('id', NumberColumnType::class) - ->addColumn('name', TextColumnType::class) - ->addColumn('createdAt', DateTimeColumnType::class) - ; - } -} -``` - -The builder's `addColumn()` method accepts _three_ arguments: - -- column name — which in most cases will represent a property path in the underlying entity; -- 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](../components/columns/types.md). - -## Making the columns sortable - -The column types are [configurable thanks to their options](../features/type-classes.md#type-configuration-options). -The options can be passed as the third argument of the builder's `addColumn()` method. -By default, columns are **not** sortable, because their [sort](../components/columns/types/column.md#sort) option equals `false`. -To change that, set the option to `true`: - -```php #10 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 - ->addColumn('id', NumberColumnType::class, [ - 'sort' => true, - ]) - ->addColumn('name', TextColumnType::class) - ->addColumn('createdAt', DateTimeColumnType::class) - ; - } -} -``` - -Now that the data table contains some columns, let's [render it](../basic-usage/rendering-the-table.md) to the user. diff --git a/docs/basic-usage/creating-data-tables.md b/docs/basic-usage/creating-data-tables.md deleted file mode 100755 index 6d0ddccb..00000000 --- a/docs/basic-usage/creating-data-tables.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -order: b ---- - -# Creating data tables - -To create a data table, either: - -* use the trait to gain access to helpful methods; -* inject data table factory and use it directly; - -For the sake of simplicity, the documentation uses the trait method: - -```php # src/Controller/ProductController.php -use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - -class ProductController extends AbstractController -{ - use DataTableFactoryAwareTrait; -} -``` - -The trait gives access to three helper methods: - -| Method | Description | -|--------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `createDataTable` | Creates data table using the type class. | -| `createNamedDataTable` | Creates data table using the type classes, but explicitly sets its name. Used in cases where the single page displays multiple data tables of the same type. | -| `createDataTableBuilder` | Creates a builder to describe the data table manually, without type classes. In most cases it is used for prototyping rather than actual usage. | - -Therefore, to create a data table, we need to create a data table type class. - -## Creating data table type classes - -The data table type classes work as a blueprint. A single type can be used to create as many data tables as needed - making them a nice, reusable piece of code. -Those classes implement the [DataTableTypeInterface](), however, it is recommended to extend them from the [AbstractDataTableType](), which already implements the interface and provides some utilities. - -```php # src/DataTable/Type/ProductDataTableType.php -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; - -class ProductDataTableType extends AbstractDataTableType -{ -} -``` - -Now, that the data table type class has been created, it can be used in the controller: - -```php # src/Controller/ProductController.php -use App\DataTable\Type\ProductDataTableType; -use Kreyu\Bundle\DataTableBundle\DataTableControllerTrait; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - -class ProductController extends AbstractController -{ - use DataTableControllerTrait; - - public function index() - { - $dataTable = $this->createDataTable(ProductDataTableType::class); - } -} -``` - -Running the code will result in an error: - -> The data table has no proxy query. You must provide it using either the data table factory or the builder "setQuery()" method. - -This is because we haven't passed anything the data table can work on. -Since we are using Doctrine ORM, the query builder should be passed as the "query" argument: - -```php #14 src/Controller/ProductController.php -use App\DataTable\Type\ProductDataTableType; -use App\Repository\ProductRepository; -use Kreyu\Bundle\DataTableBundle\DataTableControllerTrait; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - -class ProductController extends AbstractController -{ - use DataTableControllerTrait; - - public function index(ProductRepository $repository) - { - $dataTable = $this->createDataTable( - type: ProductDataTableType::class, - query: $repository->createQueryBuilder('product'), - ); - } -} -``` - -Running the code again will result in yet another error: - -> The data table has no configured columns. You must provide them using the builder "addColumn()" method. - -The message is self-explanatory - the data table has no configured columns - it is time to [add some of those](adding-columns). diff --git a/docs/basic-usage/defining-the-filters.md b/docs/basic-usage/defining-the-filters.md deleted file mode 100755 index 1ecf6cad..00000000 --- a/docs/basic-usage/defining-the-filters.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -order: e ---- - -# Defining the filters - -One of the most important features of the data tables is the ability to filter the data. -Similar to data tables and its columns, the filters are defined using the [type classes](../features/type-classes.md). - -## Adding filters to the data table - -Let's start by adding a filter for each field in the product entity: - -```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 added before... - - $builder - ->addFilter('id', NumericFilterType::class) - ->addFilter('name', StringFilterType::class) - ->addFilter('createdAt', DateRangeFilterType::class) - ; - } -} -``` - -The builder's `addFilter()` method accepts _three_ arguments: - -- filter name — which in most cases will represent a property path in the underlying entity; -- filter type — with a fully qualified class name; -- filter options — defined by the filter type, used to configure the filter; - -For reference, see [built-in filter types](../reference/filters/types.md). - -Being on the subject of filters, let's continue by enabling a [global search feature](enabling-global-search.md). diff --git a/docs/basic-usage/disclaimer.md b/docs/basic-usage/disclaimer.md deleted file mode 100755 index 1ddaa36b..00000000 --- a/docs/basic-usage/disclaimer.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -order: a ---- - -# Disclaimer - -This chapter explains how to quickly start using the bundle. - -!!! Note -The articles are **not** representing every bundle feature to the fullest. -Instead, they contain links to the reference section, where you can about each feature in depth. -!!! - -## Entities - -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 - { - return $this->id; - } - - public function getName(): string - { - return $this->name; - } - - public function getCreatedAt(): \DateTimeInterface - { - return $this->createdAt; - } -} -``` - -## 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 deleted file mode 100755 index cfb4f146..00000000 --- a/docs/basic-usage/enabling-global-search.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -order: f ---- - -# Enabling global search - -![Search filter input with the Tabler theme](./../static/global_search.png)-- - -While the filters may be useful in many cases, they are tied to a specific fields. -Sometimes all the user needs is a single text input, to quickly search through multiple fields. - -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 handler - -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; -use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQuery; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder - ->setSearchHandler($this->handleSearchFilter(...)) - ; - } - - 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("$alias.id", ":$parameter"), - $query->expr()->like("$alias.name", ":$parameter"), - ); - - $query - ->andWhere($criteria) - ->setParameter($parameter, "%$search%") - ; - } -} -``` - -!!! -**Tip**: Move the search handler logic into repository to reduce the type class complexity. -!!! - -Now that the data table has full filtration and search capabilities, let's focus on something that may be really important in some use cases — [exporting the data](exporting-the-data.md). diff --git a/docs/basic-usage/enabling-persistence.md b/docs/basic-usage/enabling-persistence.md deleted file mode 100755 index b4d8eceb..00000000 --- a/docs/basic-usage/enabling-persistence.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -order: i ---- - -# Personalizing the table - -Although the product table shown in the examples is tiny, imagine that it actually contains dozens of other columns — making it quickly unreadable! -In addition, each user may prefer a different order of these columns. -This is where the personalization functionality comes to the rescue, allowing you to freely show or hide the columns, and even determine their order. - -## Enabling the personalization feature - -The personalization feature is disabled for each data table by default. -There are multiple ways to configure the personalization feature, but for now, let's do it globally. -Navigate to the package configuration file (or create one if it doesn't exist) and change it like so: - -+++ YAML -```yaml # config/packages/kreyu_data_table.yaml -kreyu_data_table: - defaults: - personalization: - enabled: true -``` -+++ PHP -```php # config/packages/kreyu_data_table.php -use Symfony\Config\KreyuDataTableConfig; - -return static function (KreyuDataTableConfig $config) { - $defaults = $config->defaults(); - $defaults->personalization()->enabled(true); -}; -``` -+++ - -The personalization feature may look really handy, but try refreshing the page after applying the personalization - it's gone! -Now imagine configuring it on every request as the user - nightmare :ghost: -This can be solved by [enabling the persistence feature](../basic-usage/persisting-applied-data.md), -which will save the personalization data (and even the applied pagination, sorting and filters if you wish!) between requests, per user. diff --git a/docs/basic-usage/exporting-the-data.md b/docs/basic-usage/exporting-the-data.md deleted file mode 100755 index 4a581963..00000000 --- a/docs/basic-usage/exporting-the-data.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -order: g ---- - -# Exporting the data - -Ability to export the data is crucial in some use cases. The exporters handle the exporting process. -Similar to data tables and its columns and filters, exporters are defined using the [type classes](../features/type-classes.md). - -![Export modal with the Tabler theme](./../static/export_modal.png) - -## Prerequisites - -The bundle comes with exporter types using the PhpSpreadsheet. -This library is not included as a bundle dependency, therefore, make sure it is installed: - -:::flex -```bash -$ composer require phpoffice/phpspreadsheet -``` -::: - - -!!! Enabling the feature does not mean that any column will be exportable by itself. -By default, columns **are not** exportable. -!!! - -### Making the columns exportable - -To make any column exportable, use its `export` option: - -```php # src/DataTable/Type/ProductDataTableType.php -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; -use Kreyu\Bundle\DataTableBundle\Column\Type\NumberColumnType; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder - ->addColumn('id', NumberColumnType::class, [ - 'export' => true, - ]) - ; - } -} -``` - -## Adding exporters to the data table - -To add exporter, use the builder's `addExporter()` 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 - { - // Columns and filters added before... - - $builder - ->addExporter('csv', CsvExporterType::class) - ->addExporter('xlsx', XlsxExporterType::class) - ; - } -} -``` - -The builder's `addExporter()` method accepts _three_ arguments: - -- exporter name; -- exporter type — with a fully qualified class name; -- exporter options — defined by the exporter type, used to configure the exporter; - -For reference, see [built-in exporter types](../reference/exporters/types.md). - -## Downloading the file - -To download an export file, use the `export()` method on the data table. - -If you're using data tables in controllers, use it in combination with `isExporting()` method: - -```php #15-17 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); - $dataTable->handleRequest($request); - - if ($dataTable->isExporting()) { - return $this->file($dataTable->export()); - } - } -} -``` diff --git a/docs/basic-usage/index.yml b/docs/basic-usage/index.yml deleted file mode 100755 index 6e336ba5..00000000 --- a/docs/basic-usage/index.yml +++ /dev/null @@ -1,2 +0,0 @@ -icon: book -order: 4 \ No newline at end of file diff --git a/docs/basic-usage/internationalization.md b/docs/basic-usage/internationalization.md deleted file mode 100755 index d0e437fc..00000000 --- a/docs/basic-usage/internationalization.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -order: k ---- - -# Internationalization - -Internationalization is the cornerstone of any complex application - regardless of whether only one language is initially planned, the safe solution generally seems to be to include all labels into translation files. - -The data tables are supporting the Symfony Translation component out of the box. The built-in themes contains labels translated using the `KreyuDataTable` domain. Currently, there's two locales supported by the bundle: - -* Polish (pl) -* English (en) - -## Changing the built-in themes locale - -The bundle respects locale set in the framework. See official documentation on [how to configure the translation component](https://symfony.com/doc/current/translation.html#configuration). - -## Overwriting the built-in translations - -The bundle (and its themes) uses the `KreyuDataTable` translation domain. To overwrite the translations inside this domain, see official documentation on [how to overwrite a third-party bundle translations](https://symfony.com/doc/current/bundles/override.html#translations). - -## Changing the data table type translation domain - -There are multiple labels that can be translated — for example, column headers or filter names. -By default, every label is translated using the `messages` domain. -Similar to Symfony Forms component, to change the default translation domain of a data table, change the default value of `translation_domain` option in the type: - -```php # src/DataTable/Type/ProductDataTableType.php -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Symfony\Component\OptionsResolver\OptionsResolver; - -class ProductDataTableType extends AbstractDataTableType -{ - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'translation_domain' => 'products', - ]); - } -} -``` - -## Changing the column type translation domain - -The column types are little different from the data table types — instead of `translation_domain` option, these contain two: - -* `header_translation_domain` - used to translate the column headers, e.g. "Created At" into "Creation date" — by default, inherits the domain from the data table itself; -* `value_translation_domain` - used to translate the column values, for example, in case of boolean column type, it translates the "Yes" and "No" strings — it does not inherit the domain from the data table, but uses the `KreyuDataTable`. - -There may be some cases, where a single column may use a different translation domain. Let's assume that the `dates` translation domain contains strings related with dates, therefore, the `createdAt` column should use the `dates` domain: - -```php # src/DataTable/Type/ProductDataTableType.php -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Kreyu\Bundle\DataTableBundle\Column\Type\DateTimeColumnType; -use Kreyu\Bundle\DataTableBundle\Column\Type\NumberColumnType; -use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType; -use Symfony\Component\OptionsResolver\OptionsResolver; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder - // This column header will use the "products" - ->addColumn('id', NumberColumnType::class) - // This column header will use the "products" - ->addColumn('name', TextColumnType::class) - // This column header will use the "dates" - ->addColumn('createdAt', DateTimeColumnType::class, [ - 'header_translation_domain' => 'dates', - ]) - ; - } - - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'translation_domain' => 'products', - ]); - } -} -``` - -The internationalization chapter ends the basic usage section of the documentation. -Now, [continue to summary](summary.md) to see what else may be important to read from now -to configure the data tables even further :rocket: diff --git a/docs/basic-usage/persisting-applied-data.md b/docs/basic-usage/persisting-applied-data.md deleted file mode 100755 index a5c8ca8d..00000000 --- a/docs/basic-usage/persisting-applied-data.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -order: j ---- - -# Enabling persistence - -In complex applications, it can be very helpful to retain data such as applied personalization, filters, applied sorting or at least the currently displayed page. The bundle comes with the persistence feature, which can be freely configured for each feature individually. - -Let's focus on persisting the applied personalization data first. - -## Prerequisites - -For a basic usage, we're assuming that the persistence data will be saved to a **cache**, and are saved individually per **user**. -Therefore, make sure the [Symfony Cache](https://symfony.com/doc/current/components/cache.html) and [Security](https://symfony.com/doc/current/security.html) components are installed and enabled. -The bundle will automatically use them for persistence. - -## Enabling the persistence feature - -The personalization [persistence feature](../features/persistence.md) is disabled for each data table by default. -There are multiple ways to configure the persistence feature, but let's do it globally. -Navigate to the package configuration file (or create one if it doesn't exist) and change it like so: - -+++ YAML -```yaml # config/packages/kreyu_data_table.yaml -kreyu_data_table: - defaults: - personalization: - persistence_enabled: true -``` -+++ PHP -```php # config/packages/kreyu_data_table.php -use Symfony\Config\KreyuDataTableConfig; - -return static function (KreyuDataTableConfig $config) { - $defaults = $config->defaults(); - $defaults->personalization()->persistenceEnabled(true); -}; -``` -+++ - -Assuming that the user is authenticated, apply the personalization data again, refresh the page... the applied personalization is still there! - -This basic example barely scratches the surface of the [persistence feature](../features/persistence.md). -You can also persist applied pagination (e.g. current page), sorting, filters, -use different adapters (to, for example, save the data to the database, instead of cache), -or even use different subject providers (to, for example, not rely on authenticated user, but on the request IP). - -There's still one thing to walk through — let's [translate the data table to multiple languages](../basic-usage/internationalization.md). diff --git a/docs/basic-usage/rendering-the-table.md b/docs/basic-usage/rendering-the-table.md deleted file mode 100755 index 00c7c09c..00000000 --- a/docs/basic-usage/rendering-the-table.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -order: d ---- - -# Rendering the table - -The data table is created, therefore, the next step is to render it to the user. - -## Creating data table view - -First, pass the data table view to the template. -The data table view is somewhat a read-only representation of a table. -It is created using the `createView()` method: - -```php #18 src/Controller/ProductController.php -use App\DataTable\Type\ProductDataTableType; -use App\Repository\ProductRepository; -use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - -class ProductController extends AbstractController -{ - use DataTableFactoryAwareTrait; - - public function index(ProductRepository $repository) - { - $dataTable = $this->createDataTable( - type: ProductDataTableType::class, - query: $repository->createQueryBuilder('product') - ); - - return $this->render('product/index.html.twig', [ - 'data_table' => $dataTable->createView(), - ]); - } -} -``` - -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. - -## Selecting a theme - -Unfortunately, the rendered data table looks _**awful**._ This is because the default theme is being used, which contains only the HTML necessary to base a custom themes on. To fix that, create bundle configuration file and specify desired theme: - -+++ YAML -```yaml # config/packages/kreyu_data_table.yaml -kreyu_data_table: - themes: - - '@KreyuDataTable/themes/tabler.html.twig' -``` -+++ PHP -```php # config/packages/kreyu_data_table.php -use Symfony\Config\KreyuDataTableConfig; - -return static function (KreyuDataTableConfig $config) { - $config->themes([ - '@KreyuDataTable/themes/tabler.html.twig', - ]); -}; -``` -+++ - -The table is now rendered properly, using a [Tabler UI Kit](https://tabler.io/) theme. -For reference, see [built-in themes](../features/theming.md#built-in-themes). - -!!!warning -The bundle **does not** contain the CSS libraries themselves! \ -These **must** be installed and configured individually in the project. -!!! - -## Binding request to the data table - -Now, when trying to sort the data table by the ID column, **nothing happens** - this is because the data table has _no clue_ the sorting occurred! To fix that, return back to the controller, and use the handy `handleRequest()` method: - -```php #18 src/Controller/ProductController.php -use App\DataTable\Type\ProductDataTableType; -use App\Repository\ProductRepository; -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, ProductRepository $repository) - { - $dataTable = $this->createDataTable( - type: ProductDataTableType::class, - query: $repository->createQueryBuilder('product') - ); - - $dataTable->handleRequest($request); - - return $this->render('product/index.html.twig', [ - 'data_table' => $dataTable->createView(), - ]); - } -} -``` - -Now the data table is fully interactive, by having access to the request object. - -Speaking of interactivity, let's let the user [filter the table](../basic-usage/defining-the-filters.md). diff --git a/docs/basic-usage/summary.md b/docs/basic-usage/summary.md deleted file mode 100755 index 568120d6..00000000 --- a/docs/basic-usage/summary.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -order: l ---- - -# Summary - -The basic usage section went through every available feature in a _very_ basic manner — only explaining the absolute minimum required to configure a data table. -It may suit some cases, but this bundle is much, _much_ more extensive - read through the rest of the documentation to get better understanding of how things work: - -* [Type classes](../features/type-classes.md) - to understand how the types and their extensions work; -* [Persistence](../features/persistence.md) - for a deep dive in the persistence system and its adapters and subjects; -* [Proxy queries](../features/proxy-queries.md) - to learn how is the bundle separated from specific ORM and how to integrate custom data sources; -* [Symfony UX Turbo](../features/symfony-ux-turbo.md) - to understand how asynchronicity works out-of-the-box; -* [Theming](../features/theming.md) - to make the data tables fit right into your application; - -And if something goes wrong, check the [troubleshooting](../troubleshooting.md) section. \ No newline at end of file diff --git a/docs/contributing.md b/docs/contributing.md deleted file mode 100644 index 1842b027..00000000 --- a/docs/contributing.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -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 deleted file mode 100755 index ae7cc228..00000000 --- a/docs/features/actions/actions.md +++ /dev/null @@ -1,9 +0,0 @@ -# Actions - -In this bundle, there are _three_ kinds of actions: - -* [global actions](global-actions.md), displayed above the data table - e.g. "Create new user"; -* [batch actions](batch-actions.md), requiring the user to select one or more rows, e.g. "Delete selected"; -* [row actions](row-actions.md), displayed on each row, e.g. "Edit", "Delete"; - -Similar to data tables, columns and filters, actions are defined using the [type classes](../../features/type-classes.md). diff --git a/docs/features/actions/batch-actions.md b/docs/features/actions/batch-actions.md deleted file mode 100755 index 30363c4d..00000000 --- a/docs/features/actions/batch-actions.md +++ /dev/null @@ -1,405 +0,0 @@ ---- -order: b ---- - -# Batch actions - -Batch actions are special actions that hold reference to the user-selected rows. - -## Prerequisites - -To begin with, make sure the [Symfony UX integration is enabled](../installation.md#enable-the-symfony-ux-integration). -Then, enable the `batch` controller: - -```json # assets/controllers.json -{ - "controllers": { - "@kreyu/data-table-bundle": { - "batch": { - "enabled": true - } - } - } -} -``` - -## Adding batch actions - -To add batch action, use data table builder's `addBatchAction()` method: - -```php # src/DataTable/Type/ProductDataTableType.php -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Kreyu\Bundle\DataTableBundle\Action\Type\FormActionType; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder->addBatchAction('remove', FormActionType::class, [ - 'action' => '/products', - 'method' => 'DELETE', - ]); - } -} -``` - -The same method can also be used on already created data tables: - -```php #20-22 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; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; - -class ProductController extends AbstractController -{ - use DataTableFactoryAwareTrait; - - public function __construct( - private UrlGeneratorInterface $urlGenerator, - ) { - } - - public function index(Request $request) - { - $dataTable = $this->createDataTable(ProductDataTableType::class); - - $dataTable->addBatchAction('remove', FormActionType::class, [ - 'action' => '/products', - 'method' => 'DELETE', - ]); - } -} -``` - -The builder's `addBatchAction()` method accepts *three* arguments: - -- action name; -- action type - with a fully qualified class name; -- action options - defined by the action type, used to configure the action; - -For reference, see [built-in action types](../../reference/actions/types.md). - -## Adding checkbox column - -Batch actions require the user to select specific rows. This is handled by the [CheckboxColumnType](../../reference/columns/types/checkbox.md), -which simply renders a checkbox with value set to row identifier. To help with that process, -if at least one batch action is defined, this checkbox column will be added automatically. - -This column will be named `__batch`, which can be referenced using the constant: - -```php -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; - -$column = $builder->getColumn(DataTableBuilderInterface::BATCH_CHECKBOX_COLUMN_NAME); -``` - -This behavior can be disabled (or enabled back again) using the builder's method: - -```php -$builder->setAutoAddingBatchCheckboxColumn(false); -``` - -## Removing batch actions - -To remove existing batch action, use the builder's `removeBatchAction()` method: - -```php #14 src/DataTable/Type/ProductDataTableType.php -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; - -class ProductDataTableType extends AbstractDataTableType -{ - public function __construct( - private UrlGeneratorInterface $urlGenerator, - ) { - } - - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder->removeBatchAction('remove'); - } -} -``` - -The same method can also be used on already created data tables: - -```php #14 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); - - $dataTable->removeBatchAction('remove'); - } -} -``` - -Any attempt of removing the non-existent batch action will silently fail. - -## Retrieving batch actions - -To retrieve already defined batch actions, use the builder's `getBatchActions()` or `getBatchAction()` 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 batch actions: - $actions = $builder->getBatchActions(); - - // or specific batch action: - $action = $builder->getBatchAction('remove'); - - // or simply check whether the batch action is defined: - if ($builder->hasBatchAction('remove')) { - // ... - } - } -} -``` - -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 batch actions: - $actions = $dataTable->getBatchActions(); - - // or specific batch action: - $action = $dataTable->getBatchAction('remove'); - - // or simply check whether the batch action is defined: - if ($dataTable->hasBatchAction('remove')) { - // ... - } - } -} -``` - -!!!warning Warning -Any attempt of retrieving a non-existent action will result in an `OutOfBoundsException`. -To check whether the batch action of given name exists, use the `hasBatchAction()` method. -!!! - -!!!danger Important -Within the data table builder, the actions are still in their build state! -Therefore, actions retrieved by the methods: - -- `DataTableBuilderInterface::getBatchActions()` -- `DataTableBuilderInterface::getBatchAction(string $name)` - -...are instance of `ActionBuilderInterface`, whereas methods: - -- `DataTableInterface::getBatchActions()` -- `DataTableInterface::getBatchAction(string $name)` - -...return instances of `ActionInterface` instead. -!!! - -## Changing the identifier property - -By default, the checkbox column type will try to retrieve the identifier on the `id` property path. -This can be changed similarly to other column types, by providing the `property_path` option: - -```php -$builder->addColumn('__batch', CheckboxColumnType::class, [ - 'property_path' => 'uuid', -]); -``` - -If property accessor is not enough, use the `getter` option: - -```php -$builder->addColumn('__batch', CheckboxColumnType::class, [ - 'getter' => fn (Product $product) => $product->getUuid(), -]); -``` - -## Changing the identifier parameter name - -By default, the checkbox column type will add the `id` parameter to the batch actions. -For example, checking rows with ID 1, 2 will result in: - -- every batch action's `href` parameter appended with `id[]=1&id[]=2` -- every batch action's `data-id` parameter set to `[1,2]` - -The parameter name can be changed by providing the `identifier_name` option: - -```php -$builder->addColumn('__batch', CheckboxColumnType::class, [ - 'identifier_name' => 'product_id', -]); -``` - -Using the above configuration, checking rows with ID 1, 2 will result in: - -- every batch action's `href` parameter appended with `product_id[]=1&product_id[]=2` -- every batch action's `data-product-id` parameter set to `[1,2]` - -If the action has no `href` parameter, the query parameters will not be appended. -The data parameters are not used internally and can be used for custom scripts. - -If `FormActionType` is used, the scripts will append hidden inputs with selected values, for example: - -```html - - -``` - -## Adding multiple checkbox columns - -Using multiple checkbox columns for a single data table is supported. - -For example, using the following configuration: - -```php # src/DataTable/Type/ProductDataTableType.php -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Kreyu\Bundle\DataTableBundle\Column\Type\CheckboxColumnType; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder - ->addColumn('productId', CheckboxColumnType::class, [ - 'property_path' => 'id', - 'identifier_name' => 'product_id', - ]) - ->addColumn('categoryId', CheckboxColumnType::class, [ - 'property_path' => 'category.id', - 'identifier_name' => 'category_id', - ]) - ; - } -} -``` - -And having a data set which consists of two rows: - -| Product ID | Category ID | -|------------|-------------| -| 1 | 3 | -| 2 | 4 | - -Checking the first row's product and second row's category will result in: - -- every batch action's `href` parameter appended with `product_id[]=1&category_id[]=4` -- every batch action's `data-product-id` parameter set to `[1]` and `data-category-id` set to `[4]` - -If the action has no `href` parameter, the query parameters will not be appended. -The data parameters are not used internally and can be used for custom scripts. - -If `FormActionType` is used, the scripts will append hidden inputs with selected values, for example: - -```html - - -``` - -## Adding action confirmation - -Actions can be configured to require confirmation (by the user) before being executed. - -![Action confirmation modal with the Tabler theme](../../static/action_confirmation_modal.png) - -To enable confirmation in the quickest way, set the action's `confirmation` option to `true`: - -```php #10 src/DataTable/Type/ProductDataTableType.php -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Kreyu\Bundle\DataTableBundle\Action\Type\FormActionType; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder->addBatchAction('remove', FormActionType::class, [ - 'confirmation' => true, - ]); - } -} -``` - -To configure the confirmation modal, pass the array as the `confirmation` option: - -```php #10-17 src/DataTable/Type/ProductDataTableType.php -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Kreyu\Bundle\DataTableBundle\Action\Type\FormActionType; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder->addBatchAction('remove', FormActionType::class, [ - 'confirmation' => [ - 'translation_domain' => 'KreyuDataTable', - 'label_title' => 'Action confirmation', - 'label_description' => 'Are you sure you want to execute this action?', - 'label_confirm' => 'Confirm', - 'label_cancel' => 'Cancel', - 'type' => 'danger', // "danger", "warning" or "info" - ], - ]); - } -} -``` - -For reference, see [action's `confirmation` option documentation](../../reference/actions/types/action/#confirmation). - -## Conditionally rendering the action - -Action visibility can be configured using its [`visible` option](../../reference/actions/types/action/#visible): - -```php -use Kreyu\Bundle\DataTableBundle\Action\Type\FormActionType; - -$builder - ->addBatchAction('remove', FormActionType::class, [ - 'visible' => $this->isGranted('ROLE_ADMIN'), - ]) -; -``` - -Another approach would be simply not adding the action at all: - -```php -use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; - -if ($this->isGranted('ROLE_ADMIN')) { - $builder->addBatchAction('remove', FormActionType::class); -} -``` - -What differentiates those two methods, is that by using the `visible` option, the action is still defined in the data table, but is not rendered in the view. -It may be useful in some cases, for example, when the actions can be modified outside the data table builder. - diff --git a/docs/features/actions/global-actions.md b/docs/features/actions/global-actions.md deleted file mode 100755 index ff5abdfd..00000000 --- a/docs/features/actions/global-actions.md +++ /dev/null @@ -1,272 +0,0 @@ ---- -order: a ---- - -# Global actions - -Global actions are actions in the context of the whole data table, not tied to any specific row. - -## Adding global actions - -To add global action, use data table builder's `addAction()` method: - -```php #15-18 src/DataTable/Type/ProductDataTableType.php -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; - -class ProductDataTableType extends AbstractDataTableType -{ - public function __construct( - private UrlGeneratorInterface $urlGenerator, - ) { - } - - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder->addAction('create', ButtonActionType::class, [ - 'label' => 'Create new product', - 'href' => $this->urlGenerator->generate('app_product_create'), - ]); - } -} -``` - -The same method can also be used on already created data tables: - -```php #20-23 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; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; - -class ProductController extends AbstractController -{ - use DataTableFactoryAwareTrait; - - public function __construct( - private UrlGeneratorInterface $urlGenerator, - ) { - } - - public function index(Request $request) - { - $dataTable = $this->createDataTable(ProductDataTableType::class); - - $dataTable->addAction('create', ButtonActionType::class, [ - 'label' => 'Create new product', - 'href' => $this->urlGenerator->generate('app_product_create'), - ]); - } -} -``` - -This method accepts _three_ arguments: - -- action name; -- action type — with a fully qualified class name; -- action options — defined by the action type, used to configure the action; - -For reference, see [built-in action types](../../reference/actions/types.md). - -## Removing global actions - -To remove existing global action, use the builder's `removeAction()` method: - -```php #14 src/DataTable/Type/ProductDataTableType.php -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; - -class ProductDataTableType extends AbstractDataTableType -{ - public function __construct( - private UrlGeneratorInterface $urlGenerator, - ) { - } - - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder->removeAction('create'); - } -} -``` - -The same method can also be used on already created data tables: - -```php #14 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); - - $dataTable->removeAction('create'); - } -} -``` - -Any attempt of removing the non-existent action will silently fail. - -## Retrieving global actions - -To retrieve already defined global actions, use the builder's `getActions()` or `getAction()` 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 actions: - $actions = $builder->getActions(); - - // or specific action: - $action = $builder->getAction('create'); - - // or simply check whether the action is defined: - if ($builder->hasAction('create')) { - // ... - } - } -} -``` - -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 actions: - $actions = $dataTable->getActions(); - - // or specific action: - $action = $dataTable->getAction('create'); - - // or simply check whether the action is defined: - if ($dataTable->hasAction('create')) { - // ... - } - } -} -``` - -!!!warning Warning -Any attempt of retrieving a non-existent action will result in an `OutOfBoundsException`. -To check whether the global action of given name exists, use the `hasAction()` method. -!!! - -!!!danger Important -Within the data table builder, the actions are still in their build state! -Therefore, actions retrieved by the methods: - -- `DataTableBuilderInterface::getActions()` -- `DataTableBuilderInterface::getAction(string $name)` - -...are instance of `ActionBuilderInterface`, whereas methods: - -- `DataTableInterface::getActions()` -- `DataTableInterface::getAction(string $name)` - -...return instances of `ActionInterface` instead. -!!! - -## Adding action confirmation - -Actions can be configured to require confirmation (by the user) before being executed. - -![Action confirmation modal with the Tabler theme](../../static/action_confirmation_modal.png) - -To enable confirmation in the quickest way, set the action's `confirmation` option to `true`: - -```php #10 src/DataTable/Type/ProductDataTableType.php -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder->addAction('create', ButtonActionType::class, [ - 'confirmation' => true, - ]); - } -} -``` - -To configure the confirmation modal, pass the array as the `confirmation` option: - -```php #10-17 src/DataTable/Type/ProductDataTableType.php -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder->addAction('create', ButtonActionType::class, [ - 'confirmation' => [ - 'translation_domain' => 'KreyuDataTable', - 'label_title' => 'Action confirmation', - 'label_description' => 'Are you sure you want to execute this action?', - 'label_confirm' => 'Confirm', - 'label_cancel' => 'Cancel', - 'type' => 'danger', // "danger", "warning" or "info" - ], - ]); - } -} -``` - -For reference, see [action's `confirmation` option documentation](../../reference/actions/types/action/#confirmation). - -## Conditionally rendering the action - -Action visibility can be configured using its [`visible` option](../../reference/actions/types/action/#visible): - -```php -use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; - -$builder - ->addAction('create', ButtonActionType::class, [ - 'visible' => $this->isGranted('ROLE_ADMIN'), - ]) -; -``` - -Another approach would be simply not adding the action at all: - -```php -use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; - -if ($this->isGranted('ROLE_ADMIN')) { - $builder->addAction('create', ButtonActionType::class); -} -``` - -What differentiates those two methods, is that by using the `visible` option, the action is still defined in the data table, but is not rendered in the view. -It may be useful in some cases, for example, when the actions can be modified outside the data table builder. diff --git a/docs/features/actions/index.yml b/docs/features/actions/index.yml deleted file mode 100644 index e0daa2b4..00000000 --- a/docs/features/actions/index.yml +++ /dev/null @@ -1 +0,0 @@ -order: B \ No newline at end of file diff --git a/docs/features/actions/row-actions.md b/docs/features/actions/row-actions.md deleted file mode 100755 index 89f4a65c..00000000 --- a/docs/features/actions/row-actions.md +++ /dev/null @@ -1,301 +0,0 @@ ---- -order: c ---- - -# Row actions - -Row actions are actions in the context of the specific row. - -## Adding row actions - -To add row action, use data table builder's `addRowAction()` method: - -```php #15-21 src/DataTable/Type/ProductDataTableType.php -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; - -class ProductDataTableType extends AbstractDataTableType -{ - public function __construct( - private UrlGeneratorInterface $urlGenerator, - ) { - } - - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder->addRowAction('update', ButtonActionType::class, [ - 'href' => function (Product $product) { - return $this->urlGenerator->generate('app_product_update', [ - 'id' => $product->getId(), - ]); - }, - ]); - } -} -``` - -The same method can also be used on already created data tables: - -```php #20-26 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; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; - -class ProductController extends AbstractController -{ - use DataTableFactoryAwareTrait; - - public function __construct( - private UrlGeneratorInterface $urlGenerator, - ) { - } - - public function index(Request $request) - { - $dataTable = $this->createDataTable(ProductDataTableType::class); - - $dataTable->addRowAction('update', ButtonActionType::class, [ - 'href' => function (Product $product) { - return $this->urlGenerator->generate('app_product_update', [ - 'id' => $product->getId(), - ]); - }, - ]); - } -} -``` - -This method accepts _three_ arguments: - -- action name; -- action type — with a fully qualified class name; -- action options — defined by the action type, used to configure the action; - -For reference, see [built-in action types](../../reference/actions/types.md). - -## Adding actions column - -Row actions require a [ActionsColumnType](../../reference/columns/types/actions.md), -which simply renders given actions to the user. To help with that process, -if at least one row action is defined, this actions column will be added automatically. - -This column will be named `__actions`, which can be referenced using the constant: - -```php -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; - -$column = $builder->getColumn(DataTableBuilderInterface::ACTIONS_COLUMN_NAME); -``` - -This behavior can be disabled (or enabled back again) using the builder's method: - -```php -$builder->setAutoAddingActionsColumn(false); -``` - -## Removing row actions - -To remove existing row action, use the builder's `removeRowAction()` method: - -```php #14 src/DataTable/Type/ProductDataTableType.php -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; - -class ProductDataTableType extends AbstractDataTableType -{ - public function __construct( - private UrlGeneratorInterface $urlGenerator, - ) { - } - - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder->removeRowAction('update'); - } -} -``` - -The same method can also be used on already created data tables: - -```php #14 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); - - $dataTable->removeRowAction('update'); - } -} -``` - -Any attempt of removing the non-existent row action will silently fail. - -## Retrieving row actions - -To retrieve already defined row actions, use the builder's `getRowActions()` or `getRowAction()` 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 row actions: - $actions = $builder->getRowActions(); - - // or specific row action: - $action = $builder->getRowAction('update'); - - // or simply check whether the row action is defined: - if ($builder->hasRowAction('update')) { - // ... - } - } -} -``` - -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 row actions: - $actions = $dataTable->getRowActions(); - - // or specific row action: - $action = $dataTable->getRowAction('update'); - - // or simply check whether the row action is defined: - if ($dataTable->hasRowAction('update')) { - // ... - } - } -} -``` - -!!!warning Warning -Any attempt of retrieving a non-existent action will result in an `OutOfBoundsException`. -To check whether the row action of given name exists, use the `hasRowAction()` method. -!!! - -!!!danger Important -Within the data table builder, the actions are still in their build state! -Therefore, actions retrieved by the methods: - -- `DataTableBuilderInterface::getRowActions()` -- `DataTableBuilderInterface::getRowAction(string $name)` - -...are instance of `ActionBuilderInterface`, whereas methods: - -- `DataTableInterface::getRowActions()` -- `DataTableInterface::getRowAction(string $name)` - -...return instances of `ActionInterface` instead. -!!! - -## Adding action confirmation - -Actions can be configured to require confirmation (by the user) before being executed. - -![Action confirmation modal with the Tabler theme](../../static/action_confirmation_modal.png) - -To enable confirmation in the quickest way, set the action's `confirmation` option to `true`: - -```php #10 src/DataTable/Type/ProductDataTableType.php -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder->addRowAction('update', ButtonActionType::class, [ - 'confirmation' => true, - ]); - } -} -``` - -To configure the confirmation modal, pass the array as the `confirmation` option: - -```php #10-17 src/DataTable/Type/ProductDataTableType.php -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder->addRowAction('update', ButtonActionType::class, [ - 'confirmation' => [ - 'translation_domain' => 'KreyuDataTable', - 'label_title' => 'Action confirmation', - 'label_description' => 'Are you sure you want to execute this action?', - 'label_confirm' => 'Confirm', - 'label_cancel' => 'Cancel', - 'type' => 'danger', // "danger", "warning" or "info" - ], - ]); - } -} -``` - -For reference, see [action's `confirmation` option documentation](../../reference/actions/types/action/#confirmation). - -## Conditionally rendering the action - -Action visibility can be configured using its [`visible` option](../../reference/actions/types/action/#visible): - -```php -use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; - -$builder - ->addRowAction('update', ButtonActionType::class, [ - 'visible' => $this->isGranted('ROLE_ADMIN'), - ]) -; -``` - -This will determine the visibility of the action in every row. -In some cases, it may be useful to hide the action based on the row data, for example, only for products marked as "removable". -In this case, the action's `visible` option can be set to a callable, which receives the row data as the first argument: - -```php -use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; - -$builder - ->addRowAction('update', ButtonActionType::class, [ - 'visible' => function (Product $product) { - return $product->isRemovable(); - }, - ]) -; -``` diff --git a/docs/features/columns.md b/docs/features/columns.md deleted file mode 100644 index 01f837d0..00000000 --- a/docs/features/columns.md +++ /dev/null @@ -1,180 +0,0 @@ -# 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/filtering.md b/docs/features/filtering.md deleted file mode 100755 index e0f34482..00000000 --- a/docs/features/filtering.md +++ /dev/null @@ -1,413 +0,0 @@ ---- -order: c ---- - -# Filtering - -The data tables can be _filtered_, with use of the [filters](../reference/filters/types.md). - -## Toggling the feature - -By default, the filtration feature is **enabled** for every data table. -This can be configured with the `filtration_enabled` option: - -+++ Globally (YAML) -```yaml # config/packages/kreyu_data_table.yaml -kreyu_data_table: - defaults: - filtration: - enabled: true -``` -+++ Globally (PHP) -```php # config/packages/kreyu_data_table.php -use Symfony\Config\KreyuDataTableConfig; - -return static function (KreyuDataTableConfig $config) { - $defaults = $config->defaults(); - $defaults->filtration()->enabled(true); -}; -``` -+++ For data table type -```php # src/DataTable/Type/ProductDataTable.php -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Symfony\Component\OptionsResolver\OptionsResolver; - -class ProductDataTableType extends AbstractDataTableType -{ - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'filtration_enabled' => true, - ]); - } -} -``` -+++ For specific data table -```php # src/Controller/ProductController.php -use App\DataTable\Type\ProductDataTableType; -use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - -class ProductController extends AbstractController -{ - use DataTableFactoryAwareTrait; - - public function index() - { - $dataTable = $this->createDataTable( - type: ProductDataTableType::class, - query: $query, - options: [ - 'filtration_enabled' => true, - ], - ); - } -} -``` -+++ - -## Configuring the feature persistence - -By default, the filtration feature [persistence](persistence.md) is **disabled** for every data table. - -You can configure the [persistence](persistence.md) globally using the package configuration file, or its related options: - -+++ Globally (YAML) -```yaml # config/packages/kreyu_data_table.yaml -kreyu_data_table: - defaults: - filtration: - persistence_enabled: true - # if persistence is enabled and symfony/cache is installed, null otherwise - persistence_adapter: kreyu_data_table.filtration.persistence.adapter.cache - # if persistence is enabled and symfony/security-bundle is installed, null otherwise - persistence_subject_provider: kreyu_data_table.persistence.subject_provider.token_storage -``` -+++ Globally (PHP) -```php # config/packages/kreyu_data_table.php -use Symfony\Config\KreyuDataTableConfig; - -return static function (KreyuDataTableConfig $config) { - $defaults = $config->defaults(); - $defaults->filtration() - ->persistenceEnabled(true) - // if persistence is enabled and symfony/cache is installed, null otherwise - ->persistenceAdapter('kreyu_data_table.filtration.persistence.adapter.cache') - // if persistence is enabled and symfony/security-bundle is installed, null otherwise - ->persistenceSubjectProvider('kreyu_data_table.persistence.subject_provider.token_storage') - ; -}; -``` -+++ For data table type -```php # src/DataTable/Type/ProductDataTable.php -use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface; -use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Symfony\Component\DependencyInjection\Attribute\Autowire; -use Symfony\Component\OptionsResolver\OptionsResolver; - -class ProductDataTableType extends AbstractDataTableType -{ - public function __construct( - #[Autowire(service: 'kreyu_data_table.filtration.persistence.adapter.cache')] - private PersistenceAdapterInterface $persistenceAdapter, - #[Autowire(service: 'kreyu_data_table.persistence.subject_provider.token_storage')] - private PersistenceSubjectProviderInterface $persistenceSubjectProvider, - ) { - } - - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'filtration_persistence_enabled' => true, - 'filtration_persistence_adapter' => $this->persistenceAdapter, - 'filtration_persistence_subject_provider' => $this->persistenceSubjectProvider, - ]); - } -} -``` -+++ 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 Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface; -use Symfony\Component\DependencyInjection\Attribute\Autowire; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - -class ProductController extends AbstractController -{ - use DataTableFactoryAwareTrait; - - public function __construct( - #[Autowire(service: 'kreyu_data_table.filtration.persistence.adapter.cache')] - private PersistenceAdapterInterface $persistenceAdapter, - #[Autowire(service: 'kreyu_data_table.persistence.subject_provider.token_storage')] - private PersistenceSubjectProviderInterface $persistenceSubjectProvider, - ) { - } - - public function index() - { - $dataTable = $this->createDataTable( - type: ProductDataTableType::class, - query: $query, - options: [ - 'filtration_persistence_enabled' => true, - 'filtration_persistence_adapter' => $this->persistenceAdapter, - 'filtration_persistence_subject_provider' => $this->persistenceSubjectProvider, - ], - ); - } -} -``` -+++ - -## Adding the filters - -To add a filter, use the `addFilter()` method on the data table builder: - -```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; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder - ->addFilter('id', NumericFilterType::class) - ->addFilter('name', StringFilterType::class) - ; - } -} -``` - -The builder's `addFilter()` method accepts _three_ arguments: - -- filter name — which in most cases will represent a property path in the underlying entity; -- filter type — with a fully qualified class name; -- filter options — defined by the filter type, used to configure the filter; - -For reference, see [built-in filter types](../reference/filters/types.md). - -### Specifying the query path - -The bundle will use the filter name as the path to perform filtration on. -However, if the path is different from the column name, provide it using the `query_path` 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\StringFilterType; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder - ->addFilter('category', StringFilterType::class, [ - 'query_path' => 'category.name', - ]) - ; - } -} -``` - -For reference, see [built-in filter types](../reference/filters/types.md). - -## Filter operators - -Each filter can support multiple operators, such as "equals", "contains", "starts with", etc. -Optionally, the filtration form can display the operator selector, letting the user select a desired filtration method. - -### **Default operator** - -The default operator can be configured using the `default_operator` 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, [ - '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; -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; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder - ->addFilter('id', NumericFilterType::class) - ->addFilter('name', StringFilterType::class, [ - '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, - ], - ]) - ; - } -} -``` - -Remember that each filter can support a different set of operators internally! - -## Configuring form type - -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 -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder - ->addFilter('id', NumericFilterType::class) - ->addFilter('name', StringFilterType::class, [ - 'form_type' => SearchType::class, - 'form_options' => [ - 'attr' => [ - 'placeholder' => 'Name', - ], - ], - ]) - ; - } -} -``` - -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 Symfony\Component\Form\Extension\Core\Type\ChoiceType; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder - ->addFilter('id', NumericFilterType::class) - ->addFilter('name', StringFilterType::class, [ - 'operator_form_type' => ChoiceType::class, - 'operator_form_options' => [ - 'attr' => [ - 'placeholder' => 'Operator', - ], - ], - ]) - ; - } -} -``` - -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: - -```php # src/DataTable/Type/ProductDataTableType.php -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Kreyu\Bundle\DataTableBundle\Filter\FiltrationData; -use Kreyu\Bundle\DataTableBundle\Filter\FilterData; -use Kreyu\Bundle\DataTableBundle\Filter\Operator; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder->setDefaultFiltrationData(new FiltrationData([ - 'id' => new FilterData(value: 1, operator: Operator::Contains), - ])); - - // or by creating the filtration data from an array: - $builder->setDefaultFiltrationData(FiltrationData::fromArray([ - 'id' => ['value' => 1, 'operator' => 'contains'], - ])); - } -} -``` - -## 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 deleted file mode 100755 index 324cfc12..00000000 --- a/docs/features/global-search.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -order: f ---- - -# Global search - -![Search filter input with the Tabler theme](./../static/global_search.png)-- - -While the filters may be useful in many cases, they are tied to a specific fields. -Sometimes all the user needs is a single text input, to quickly search through multiple fields. - -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 handler - -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; -use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQuery; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder - ->setSearchHandler($this->handleSearchFilter(...)) - ; - } - - 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("$alias.id", ":$parameter"), - $query->expr()->like("$alias.name", ":$parameter"), - ); - - $query - ->andWhere($criteria) - ->setParameter($parameter, "%$search%") - ; - } -} -``` - -!!! -**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 deleted file mode 100755 index 63ec3391..00000000 --- a/docs/features/index.yml +++ /dev/null @@ -1,3 +0,0 @@ -order: 4 -icon: package -expanded: true diff --git a/docs/features/proxy-queries.md b/docs/features/proxy-queries.md deleted file mode 100755 index 8a31ec74..00000000 --- a/docs/features/proxy-queries.md +++ /dev/null @@ -1,167 +0,0 @@ ---- -order: i ---- - -# Proxy queries - -This bundle is data source agnostic, meaning it is not tied to any specific ORM, such as Doctrine ORM. -This is accomplished thanks to **proxy queries**, which work as an adapter for the specific data source. - -For example, if you want to display a list of products from the database, and your application uses Doctrine ORM, -then you'd want to use the built-in [:icon-mark-github: DoctrineOrmProxyQuery](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQuery.php). -If your data comes from another source (from an array, from CSV, etc.), then you can create a custom proxy query class. - -## Creating custom proxy query - -To create a custom proxy query, create a class that implements [:icon-mark-github: ProxyQueryInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Query/ProxyQueryInterface.php): - -```php # src/DataTable/Query/ArrayProxyQuery.php -use Kreyu\Bundle\DataTableBundle\Pagination\PaginationInterface; -use Kreyu\Bundle\DataTableBundle\Pagination\PaginationData; -use Kreyu\Bundle\DataTableBundle\Sorting\SortingData; -use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface; - -class ArrayProxyQuery implements ProxyQueryInterface -{ - public function __construct( - private array $data, - ) { - } - - public function sort(SortingData $sortingData): void - { - // sort the data, for example, using the uksort... - } - - public function paginate(PaginationData $paginationData): void - { - // save pagination data in proxy query... - } - - public function getPagination(): PaginationInterface - { - // create new pagination using the limit iterator as items... - } -} -``` - -!!! -The recommended namespace for the proxy queries is `App\DataTable\Query`. -!!! - -Now you can use the custom proxy query when creating the data tables: - -```php #20 src/Controller/ProductController.php -use App\DataTable\Type\ProductDataTableType; -use App\DataTable\Query\ArrayProxyQuery; -use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - -class ProductController extends AbstractController -{ - use DataTableFactoryAwareTrait; - - public function index() - { - $products = [ - new Product(name: 'Product #1'), - new Product(name: 'Product #2'), - new Product(name: 'Product #3'), - ]; - - $dataTable = $this->createDataTable( - type: ProductDataTableType::class, - query: new ArrayProxyQuery($products), - ); - } -} -``` - -## Creating proxy query factory - -When using the data table factory, you can pass either the custom proxy query class, or just the data you want to operate on. -For example, if you pass Doctrine ORM's QueryBuilder class, it will be automatically converted to the [:icon-mark-github: DoctrineOrmProxyQuery](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQuery.php) object: - -```php #15-16 src/Controller/ProductController.php -use App\DataTable\Type\ProductDataTableType; -use App\DataTable\Query\ArrayProxyQuery; -use App\Repository\ProductRepository; -use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - -class ProductController extends AbstractController -{ - use DataTableFactoryAwareTrait; - - public function index(ProductRepository $repository) - { - $dataTable = $this->createDataTable( - type: ProductDataTableType::class, - // note that there's no DoctrineOrmProxyQuery, just a QueryBuilder: - query: $repository->createQueryBuilder('product'), - ); - } -} -``` - -If you try to do the same with the custom proxy query, it will result in an error: - -> Unable to create ProxyQuery for given data - -In the background, the [:icon-mark-github: ChainProxyQueryFactory](https://github.com/Kreyu/data-table-bundle/blob/main/src/Query/ChainProxyQueryFactory.php) -iterates through registered proxy query factories, and returns the first successfully created proxy query. -The error occurs because there is no factory to create the custom type. - -To create a proxy query factory, create a class that implements the [:icon-mark-github: ProxyQueryFactoryInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Query/ProxyQueryFactoryInterface.php): - -```php # src/DataTable/Query/ArrayProxyQueryFactory.php -use App\DataTable\Query\ArrayProxyQuery; -use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryFactoryInterface; -use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface; - -class ArrayProxyQueryFactory implements ProxyQueryFactoryInterface -{ - public function create(mixed $data): ProxyQueryInterface - { - if (!is_array($data)) { - throw new UnexpectedTypeException($data, ArrayProxyQuery::class); - } - - return new ArrayProxyQuery($data); - } -} -``` - -If the custom proxy query does not support a specific data class, the factory **have** to throw an [:icon-mark-github: UnexpectedTypeException](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exception/UnexpectedTypeException.php), -so the chain proxy query factory will know to skip that factory and check other ones. - -Proxy query factories must be registered as services and tagged with the `kreyu_data_table.proxy_query.factory` tag. -If you're using the [default services.yaml configuration](https://symfony.com/doc/current/service_container.html#service-container-services-load-example), -this is already done for you, thanks to [autoconfiguration](https://symfony.com/doc/current/service_container.html#services-autoconfigure). - -In above examples, now it would be possible to pass the array directly as the "query" argument: - -```php #19 src/Controller/ProductController.php -use App\DataTable\Type\ProductDataTableType; -use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - -class ProductController extends AbstractController -{ - use DataTableFactoryAwareTrait; - - public function index() - { - $products = [ - new Product(name: 'Product #1'), - new Product(name: 'Product #2'), - new Product(name: 'Product #3'), - ]; - - $dataTable = $this->createDataTable( - type: ProductDataTableType::class, - query: $products, - ); - } -} -``` diff --git a/docs/features/request-handlers.md b/docs/features/request-handlers.md deleted file mode 100755 index a31fa793..00000000 --- a/docs/features/request-handlers.md +++ /dev/null @@ -1,130 +0,0 @@ ---- -order: i ---- - -# Request handlers - -The data tables by default have no clue about the requests. -To solve this problem, a request can be handled by the data table using the `handleRequest()` method. -This means an underlying request handler will be called, extracting the required data from the request, -and calling methods such as `sort()` or `paginate()` on the data table. - -## Built-in request handlers - -This bundle comes with [:icon-mark-github: HttpFoundationRequestHandler](https://github.com/Kreyu/data-table-bundle/blob/main/src/Request/HttpFoundationRequestHandler.php), -which supports the [:icon-mark-github: request object](https://github.com/symfony/http-foundation/blob/6.2/Request.php) common for the Symfony applications: - -```php #4,13 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); - $dataTable->handleRequest($request); - } -} -``` - -## Creating request handlers - -To create a request handler, create a class that implements [:icon-mark-github: RequestHandlerInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Request/RequestHandlerInterface.php): - -```php # src/DataTable/Request/CustomRequestHandler.php -use Kreyu\Bundle\DataTableBundle\Request\RequestHandlerInterface; -use Kreyu\Bundle\DataTableBundle\Sorting\SortingData; -use Kreyu\Bundle\DataTableBundle\Sorting\SortingField; - -class CustomRequestHandler implements RequestHandlerInterface -{ - public function handle(DataTableInterface $dataTable, mixed $request = null): void - { - // Call desired methods with arguments based on the data from $request - $dataTable->paginate(...); - $dataTable->sort(...); - $dataTable->personalize(...); - $dataTable->filter(...); - $dataTable->export(...); - } -} -``` - -!!! -The recommended namespace for the request handlers is `App\DataTable\Request`. -!!! - -You can apply this request handler globally using the configuration file, or use `request_handler` option: - -+++ Globally (YAML) -```yaml # config/packages/kreyu_data_table.yaml -kreyu_data_table: - defaults: - # this should be a service id - which is class by default - request_handler: 'App\DataTable\Request\CustomRequestHandler' -``` -+++ Globally (PHP) -```php # config/packages/kreyu_data_table.php -use Symfony\Config\KreyuDataTableConfig; - -return static function (KreyuDataTableConfig $config) { - $defaults = $config->defaults(); - // this should be a service id - which is class by default - $defaults->requestHandler('App\DataTable\Request\CustomRequestHandler'); -}; -``` -+++ For data table type -```php # src/DataTable/Type/ProductDataTable.php -use App\DataTable\Request\CustomRequestHandler; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Symfony\Component\OptionsResolver\OptionsResolver; - -class ProductDataTableType extends AbstractDataTableType -{ - public function __construct( - private CustomRequestHandler $requestHandler, - ) { - } - - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'request_handler' => $this->requestHandler, - ]); - } -} -``` -+++ For specific data table -```php # src/Controller/ProductController.php -use App\DataTable\Request\CustomRequestHandler; -use App\DataTable\Type\ProductDataTableType; -use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - -class ProductController extends AbstractController -{ - use DataTableFactoryAwareTrait; - - public function __construct( - private CustomRequestHandler $requestHandler, - ) { - } - - public function index() - { - $dataTable = $this->createDataTable( - type: ProductDataTableType::class, - query: $query, - options: [ - 'request_handler' => $this->requestHandler, - ], - ); - } -} -``` -+++ diff --git a/docs/features/type-classes.md b/docs/features/type-classes.md deleted file mode 100755 index bbc4baf6..00000000 --- a/docs/features/type-classes.md +++ /dev/null @@ -1,344 +0,0 @@ ---- -order: j ---- - -# Type classes - -Multiple parts of the bundle, such as columns, filters etc. are described using the type classes. -Their purpose and method of definition is very similar to the [Symfony Form Types](https://symfony.com/doc/current/reference/forms/types.html), -which means knowing how these works really help in understanding most of the bundle. - -Following parts of the bundle are defined using the type classes: - -* data tables -* columns -* filters -* actions -* exporters - -## Creating custom 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. - -{.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` | -| Columns | `App\DataTable\Column\Type` | -| Filters | `App\DataTable\Filter\Type` | -| Actions | `App\DataTable\Action\Type` | -| Exporters | `App\DataTable\Exporter\Type` | - -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` | -| Columns | `kreyu_data_table.column.type` | -| Filters | `kreyu_data_table.filter.type` | -| Actions | `kreyu_data_table.action.type` | -| Exporters | `kreyu_data_table.exporter.type` | - -!!! Note -Custom type classes are **automatically** registered as a service. -!!! - -### Type inheritance - -For example, let's think of a column type that represents a phone number. -In theory, it should extend the existing text column type, only adding a phone number oriented formatting. -In practice, the type's class **should not** extend the text type class directly: - -!!!danger -This is invalid - do **NOT** use PHP class inheritance! - -```php # src/DataTable/Column/Type/PhoneColumnType.php -use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType; - -class PhoneColumnType extends TextColumnType -{ -} -``` -!!! - -Instead, it should return the fully qualified class name of the parent type in the `getParent()` method: - -!!!success -This is valid - extend abstract type and return parent's class name! - -```php # src/DataTable/Column/Type/PhoneColumnType.php -use Kreyu\Bundle\DataTableBundle\Column\Type\AbstractColumnType; -use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType; - -class PhoneColumnType implements AbstractColumnType -{ - public function getParent(): string - { - return TextColumnType::class; - } -} -``` -!!! - -The difference is all about the extensions. Considering the example above, while using the PHP inheritance, -a [type extensions](#defining-the-type-extensions) defined for the text column type won't be applied to the phone column type. - -### Type configuration options - -Each type class contains its own set of options, that can be used to configure the type according to a specific need. -Those options can be defined in any type class `configureOptions()` method, by using the [OptionsResolver component](https://symfony.com/doc/current/components/options_resolver.html): - -```php # src/DataTable/Column/Type/UserColumnType.php -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Security\Core\User\UserInterface; - -class UserDataTableType extends AbstractDataTableType -{ - public function configureOptions(OptionsResolver $resolver): void - { - $resolver - ->setDefault('display_role', false) - ->setAllowedTypes('display_role', 'bool') - ; - } -} -``` - -Additionally, options are inherited from the type specified in the `getParent()` method: - -```php # src/DataTable/Column/Type/AdminColumnType.php -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Security\Core\User\UserInterface; - -// This class inherits options from UserDataTableType, including "display_role" -class AdminDataTableType extends AbstractDataTableType -{ - public function getParent(): string - { - return UserDataTableType::class; - } -} -``` - -Remember that values set in `configureOptions()` using the `->setDefault()` method are **defaults**. -This means they **still** can be provided when creating, in this example, a data table: - -```php # src/Controller/UserController.php -use App\DataTable\Type\UserDataTableType; -use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - -class UserController extends AbstractController -{ - use DataTableFactoryAwareTrait; - - public function index() - { - // This data table option "display_role" equals "false", - // because it is not explicitly given, and falls back to default value. - $dataTable = $this->createDataTable(UserDataTableType::class); - - // This data table option "display_role" equals "true". - $dataTable = $this->createDataTable(UserDataTableType::class, options: [ - 'display_role' => true, - ]); - - // This data table option "display_role" equals "false", - // because it is not explicitly given, and falls back to default value, - // which is inherited from the parent type (in this case: UserDataTableType). - $dataTable = $this->createDataTable(AdminDataTableType::class); - } -} -``` - -## Defining the type extensions - -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. - -{.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 - -Each type extension class **have to** define a list of types that it extends, using the `getExtendedTypes()` method. -For example, if you wish to create an extension for a built-in text column type, consider following configuration: - -```php # src/DataTable/Column/Extension/TextColumnTypeExtension.php -use Kreyu\Bundle\DataTableBundle\Column\Extension\AbstractColumnTypeExtension; -use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType; - -class TextColumnTypeExtension extends AbstractColumnTypeExtension -{ - public static function getExtendedTypes(): iterable - { - return [TextColumnType::class]; - } -} -``` - -To apply an extension to _every type_ in the system, use the base type of each part of the bundle. -For example, in case of the column types: - -```php # src/DataTable/Column/Extension/ColumnTypeExtension.php -use Kreyu\Bundle\DataTableBundle\Column\Type\ColumnType; -use Kreyu\Bundle\DataTableBundle\Column\Extension\AbstractColumnTypeExtension; - -class ColumnTypeExtension extends AbstractColumnTypeExtension -{ - public static function getExtendedTypes(): iterable - { - return [ColumnType::class]; - } -} -``` - -For reference, a list of each feature base type class: - -{.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: - -```yaml # config/services.yaml -services: - App\DataTable\Extension\ExtensionA: - tags: - - { name: kreyu_data_table.type_extension, priority: 1 } - - App\DataTable\Extension\ExtensionB: - tags: - - { name: kreyu_data_table.type_extension, priority: 2 } -``` - -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 - -Type classes support [inheritance](#type-inheritance) and [extensions](#defining-the-type-extensions), -therefore they must be **resolved** before they can be used in the application. The resolved type classes -has direct access to an instance of the parent type (also resolved), as well as the extensions to apply. - -Each component that supports the type classes, contain its "resolved" counterpart: - -{.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: - -```php # vendor/kreyu/data-table-bundle/src/Type/ResolvedDataTableType.php -public function buildDataTable(DataTableBuilderInterface $builder, array $options): void -{ - $this->parent?->buildDataTable($builder, $options); - - $this->innerType->buildDataTable($builder, $options); - - foreach ($this->typeExtensions as $extension) { - $extension->buildDataTable($builder, $options); - } -} -``` - -Breaking it down into smaller pieces, first, the type's parent method is called: - -```php # -$this->parent?->buildDataTable($builder, $options); -``` - -The _parent_ is an instance of already resolved type. It is based on the FQCN provided in the `getParent()` method. - -Next comes the _inner type_ itself: - -```php # -$this->innerType->buildDataTable($builder, $options); -``` - -The _inner type_ is an instance of non-resolved type, provided with the FQCN when defining the data table. -It is very important to understand, that this method is called **after** the parent one, but **before** any extension. - -Last but not least, there's the extensions: - -```php # -foreach ($this->typeExtensions as $extension) { - $extension->buildDataTable($builder, $options); -} -``` - -This is why [defining an order of extensions](#setting-the-extension-order) may be very important in some cases. -Same flow applies to every resolved type class and most of its methods in the bundle. - -## Accessing the type registry - -The registries are the classes that stores all the types and extensions registered in the system. -Those classes are used to easily retrieve a [resolved types](#resolving-the-types), -while only requiring a fully qualified class name of the desired type. - -Each component that supports the type classes contains its own registry: - -{.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); diff --git a/docs/index.md b/docs/index.md deleted file mode 100755 index a70c67e0..00000000 --- a/docs/index.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -order: 6 ---- - -# Introduction - - -This bundle aims to streamline the creation process of the data tables in Symfony applications. - -!!! Note -Despite the name, this bundle has **no correlation** with [jQuery Data Tables](https://datatables.net/). -!!! - -## Features - -- [Type classes](https://data-table-bundle.swroblewski.pl/features/type-classes) for a class-based configuration, similar to [Symfony Forms](https://symfony.com/doc/current/forms.html) -- [Paginating](https://data-table-bundle.swroblewski.pl/features/pagination), [sorting](https://data-table-bundle.swroblewski.pl/features/sorting) and [filtering](https://data-table-bundle.swroblewski.pl/features/filtering) - classic triforce of the data tables -- [Personalization](https://data-table-bundle.swroblewski.pl/features/personalization) where the user decides the order and visibility of columns -- [Persistence](https://data-table-bundle.swroblewski.pl/features/persistence) to save user-applied pagination, sorting, filters and personalization -- [Exporting](https://data-table-bundle.swroblewski.pl/features/exporting) with or without applied pagination, personalization and filters -- [Theming](https://data-table-bundle.swroblewski.pl/features/theming) of every part of the bundle -- [Data source agnostic](https://data-table-bundle.swroblewski.pl/features/proxy-queries) with Doctrine ORM supported out of the box -- [Integration with Symfony UX Turbo](https://data-table-bundle.swroblewski.pl/features/symfony-ux-turbo) for asynchronicity diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 00000000..b549fff0 --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,1571 @@ +{ + "name": "docs", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "markdown-it": "^14.0.0", + "vitepress": "^1.0.0-rc.36", + "vitepress-plugin-tabs": "^0.5.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz", + "integrity": "sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.9.3", + "@algolia/autocomplete-shared": "1.9.3" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.9.3.tgz", + "integrity": "sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-shared": "1.9.3" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.9.3.tgz", + "integrity": "sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-shared": "1.9.3" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.9.3.tgz", + "integrity": "sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ==", + "dev": true, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/cache-browser-local-storage": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.22.1.tgz", + "integrity": "sha512-Sw6IAmOCvvP6QNgY9j+Hv09mvkvEIDKjYW8ow0UDDAxSXy664RBNQk3i/0nt7gvceOJ6jGmOTimaZoY1THmU7g==", + "dev": true, + "dependencies": { + "@algolia/cache-common": "4.22.1" + } + }, + "node_modules/@algolia/cache-common": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.22.1.tgz", + "integrity": "sha512-TJMBKqZNKYB9TptRRjSUtevJeQVXRmg6rk9qgFKWvOy8jhCPdyNZV1nB3SKGufzvTVbomAukFR8guu/8NRKBTA==", + "dev": true + }, + "node_modules/@algolia/cache-in-memory": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.22.1.tgz", + "integrity": "sha512-ve+6Ac2LhwpufuWavM/aHjLoNz/Z/sYSgNIXsinGofWOysPilQZPUetqLj8vbvi+DHZZaYSEP9H5SRVXnpsNNw==", + "dev": true, + "dependencies": { + "@algolia/cache-common": "4.22.1" + } + }, + "node_modules/@algolia/client-account": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.22.1.tgz", + "integrity": "sha512-k8m+oegM2zlns/TwZyi4YgCtyToackkOpE+xCaKCYfBfDtdGOaVZCM5YvGPtK+HGaJMIN/DoTL8asbM3NzHonw==", + "dev": true, + "dependencies": { + "@algolia/client-common": "4.22.1", + "@algolia/client-search": "4.22.1", + "@algolia/transporter": "4.22.1" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.22.1.tgz", + "integrity": "sha512-1ssi9pyxyQNN4a7Ji9R50nSdISIumMFDwKNuwZipB6TkauJ8J7ha/uO60sPJFqQyqvvI+px7RSNRQT3Zrvzieg==", + "dev": true, + "dependencies": { + "@algolia/client-common": "4.22.1", + "@algolia/client-search": "4.22.1", + "@algolia/requester-common": "4.22.1", + "@algolia/transporter": "4.22.1" + } + }, + "node_modules/@algolia/client-common": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.22.1.tgz", + "integrity": "sha512-IvaL5v9mZtm4k4QHbBGDmU3wa/mKokmqNBqPj0K7lcR8ZDKzUorhcGp/u8PkPC/e0zoHSTvRh7TRkGX3Lm7iOQ==", + "dev": true, + "dependencies": { + "@algolia/requester-common": "4.22.1", + "@algolia/transporter": "4.22.1" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.22.1.tgz", + "integrity": "sha512-sl+/klQJ93+4yaqZ7ezOttMQ/nczly/3GmgZXJ1xmoewP5jmdP/X/nV5U7EHHH3hCUEHeN7X1nsIhGPVt9E1cQ==", + "dev": true, + "dependencies": { + "@algolia/client-common": "4.22.1", + "@algolia/requester-common": "4.22.1", + "@algolia/transporter": "4.22.1" + } + }, + "node_modules/@algolia/client-search": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.22.1.tgz", + "integrity": "sha512-yb05NA4tNaOgx3+rOxAmFztgMTtGBi97X7PC3jyNeGiwkAjOZc2QrdZBYyIdcDLoI09N0gjtpClcackoTN0gPA==", + "dev": true, + "dependencies": { + "@algolia/client-common": "4.22.1", + "@algolia/requester-common": "4.22.1", + "@algolia/transporter": "4.22.1" + } + }, + "node_modules/@algolia/logger-common": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.22.1.tgz", + "integrity": "sha512-OnTFymd2odHSO39r4DSWRFETkBufnY2iGUZNrMXpIhF5cmFE8pGoINNPzwg02QLBlGSaLqdKy0bM8S0GyqPLBg==", + "dev": true + }, + "node_modules/@algolia/logger-console": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.22.1.tgz", + "integrity": "sha512-O99rcqpVPKN1RlpgD6H3khUWylU24OXlzkavUAMy6QZd1776QAcauE3oP8CmD43nbaTjBexZj2nGsBH9Tc0FVA==", + "dev": true, + "dependencies": { + "@algolia/logger-common": "4.22.1" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.22.1.tgz", + "integrity": "sha512-dtQGYIg6MteqT1Uay3J/0NDqD+UciHy3QgRbk7bNddOJu+p3hzjTRYESqEnoX/DpEkaNYdRHUKNylsqMpgwaEw==", + "dev": true, + "dependencies": { + "@algolia/requester-common": "4.22.1" + } + }, + "node_modules/@algolia/requester-common": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.22.1.tgz", + "integrity": "sha512-dgvhSAtg2MJnR+BxrIFqlLtkLlVVhas9HgYKMk2Uxiy5m6/8HZBL40JVAMb2LovoPFs9I/EWIoFVjOrFwzn5Qg==", + "dev": true + }, + "node_modules/@algolia/requester-node-http": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.22.1.tgz", + "integrity": "sha512-JfmZ3MVFQkAU+zug8H3s8rZ6h0ahHZL/SpMaSasTCGYR5EEJsCc8SI5UZ6raPN2tjxa5bxS13BRpGSBUens7EA==", + "dev": true, + "dependencies": { + "@algolia/requester-common": "4.22.1" + } + }, + "node_modules/@algolia/transporter": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.22.1.tgz", + "integrity": "sha512-kzWgc2c9IdxMa3YqA6TN0NW5VrKYYW/BELIn7vnLyn+U/RFdZ4lxxt9/8yq3DKV5snvoDzzO4ClyejZRdV3lMQ==", + "dev": true, + "dependencies": { + "@algolia/cache-common": "4.22.1", + "@algolia/logger-common": "4.22.1", + "@algolia/requester-common": "4.22.1" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@docsearch/css": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.5.2.tgz", + "integrity": "sha512-SPiDHaWKQZpwR2siD0KQUwlStvIAnEyK6tAE2h2Wuoq8ue9skzhlyVQ1ddzOxX6khULnAALDiR/isSF3bnuciA==", + "dev": true + }, + "node_modules/@docsearch/js": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.5.2.tgz", + "integrity": "sha512-p1YFTCDflk8ieHgFJYfmyHBki1D61+U9idwrLh+GQQMrBSP3DLGKpy0XUJtPjAOPltcVbqsTjiPFfH7JImjUNg==", + "dev": true, + "dependencies": { + "@docsearch/react": "3.5.2", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/react": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.5.2.tgz", + "integrity": "sha512-9Ahcrs5z2jq/DcAvYtvlqEBHImbm4YJI8M9y0x6Tqg598P40HTEkX7hsMcIuThI+hTFxRGZ9hll0Wygm2yEjng==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-core": "1.9.3", + "@algolia/autocomplete-preset-algolia": "1.9.3", + "@docsearch/css": "3.5.2", + "algoliasearch": "^4.19.1" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", + "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz", + "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz", + "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz", + "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz", + "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz", + "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz", + "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz", + "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz", + "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz", + "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz", + "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz", + "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz", + "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz", + "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz", + "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz", + "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz", + "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz", + "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz", + "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz", + "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz", + "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz", + "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz", + "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.4.tgz", + "integrity": "sha512-ub/SN3yWqIv5CWiAZPHVS1DloyZsJbtXmX4HxUTIpS0BHm9pW5iYBo2mIZi+hE3AeiTzHz33blwSnhdUo+9NpA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.4.tgz", + "integrity": "sha512-ehcBrOR5XTl0W0t2WxfTyHCR/3Cq2jfb+I4W+Ch8Y9b5G+vbAecVv0Fx/J1QKktOrgUYsIKxWAKgIpvw56IFNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.4.tgz", + "integrity": "sha512-1fzh1lWExwSTWy8vJPnNbNM02WZDS8AW3McEOb7wW+nPChLKf3WG2aG7fhaUmfX5FKw9zhsF5+MBwArGyNM7NA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.4.tgz", + "integrity": "sha512-Gc6cukkF38RcYQ6uPdiXi70JB0f29CwcQ7+r4QpfNpQFVHXRd0DfWFidoGxjSx1DwOETM97JPz1RXL5ISSB0pA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.4.tgz", + "integrity": "sha512-g21RTeFzoTl8GxosHbnQZ0/JkuFIB13C3T7Y0HtKzOXmoHhewLbVTFBQZu+z5m9STH6FZ7L/oPgU4Nm5ErN2fw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.4.tgz", + "integrity": "sha512-TVYVWD/SYwWzGGnbfTkrNpdE4HON46orgMNHCivlXmlsSGQOx/OHHYiQcMIOx38/GWgwr/po2LBn7wypkWw/Mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.4.tgz", + "integrity": "sha512-XcKvuendwizYYhFxpvQ3xVpzje2HHImzg33wL9zvxtj77HvPStbSGI9czrdbfrf8DGMcNNReH9pVZv8qejAQ5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.4.tgz", + "integrity": "sha512-LFHS/8Q+I9YA0yVETyjonMJ3UA+DczeBd/MqNEzsGSTdNvSJa1OJZcSH8GiXLvcizgp9AlHs2walqRcqzjOi3A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.4.tgz", + "integrity": "sha512-dIYgo+j1+yfy81i0YVU5KnQrIJZE8ERomx17ReU4GREjGtDW4X+nvkBak2xAUpyqLs4eleDSj3RrV72fQos7zw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.4.tgz", + "integrity": "sha512-RoaYxjdHQ5TPjaPrLsfKqR3pakMr3JGqZ+jZM0zP2IkDtsGa4CqYaWSfQmZVgFUCgLrTnzX+cnHS3nfl+kB6ZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.4.tgz", + "integrity": "sha512-T8Q3XHV+Jjf5e49B4EAaLKV74BbX7/qYBRQ8Wop/+TyyU0k+vSjiLVSHNWdVd1goMjZcbhDmYZUYW5RFqkBNHQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.4.tgz", + "integrity": "sha512-z+JQ7JirDUHAsMecVydnBPWLwJjbppU+7LZjffGf+Jvrxq+dVjIE7By163Sc9DKc3ADSU50qPVw0KonBS+a+HQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.4.tgz", + "integrity": "sha512-LfdGXCV9rdEify1oxlN9eamvDSjv9md9ZVMAbNHA87xqIfFCxImxan9qZ8+Un54iK2nnqPlbnSi4R54ONtbWBw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/linkify-it": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", + "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==", + "dev": true + }, + "node_modules/@types/markdown-it": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.7.tgz", + "integrity": "sha512-U/CBi2YUUcTHBt5tjO2r5QV/x0Po6nsYwQU4Y04fBS6vfoImaiZ6f8bi3CjTCxBPQSO1LMyUqkByzi8AidyxfA==", + "dev": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", + "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", + "dev": true + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "dev": true + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.2.tgz", + "integrity": "sha512-kEjJHrLb5ePBvjD0SPZwJlw1QTRcjjCA9sB5VyfonoXVBxTS7TMnqL6EkLt1Eu61RDeiuZ/WN9Hf6PxXhPI2uA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.7.tgz", + "integrity": "sha512-hhCaE3pTMrlIJK7M/o3Xf7HV8+JoNTGOQ/coWS+V+pH6QFFyqtoXqQzpqsNp7UK17xYKua/MBiKj4e1vgZOBYw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.23.6", + "@vue/shared": "3.4.7", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.7.tgz", + "integrity": "sha512-qDKBAIurCTub4n/6jDYkXwgsFuriqqmmLrIq1N2QDfYJA/mwiwvxi09OGn28g+uDdERX9NaKDLji0oTjE3sScg==", + "dev": true, + "dependencies": { + "@vue/compiler-core": "3.4.7", + "@vue/shared": "3.4.7" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.7.tgz", + "integrity": "sha512-Gec6CLkReVswDYjQFq79O5rktri4R7TsD/VPCiUoJw40JhNNxaNJJa8mrQrWoJluW4ETy6QN0NUyC/JO77OCOw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.23.6", + "@vue/compiler-core": "3.4.7", + "@vue/compiler-dom": "3.4.7", + "@vue/compiler-ssr": "3.4.7", + "@vue/shared": "3.4.7", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.5", + "postcss": "^8.4.32", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.7.tgz", + "integrity": "sha512-PvYeSOvnCkST5mGS0TLwEn5w+4GavtEn6adcq8AspbHaIr+mId5hp7cG3ASy3iy8b+LuXEG2/QaV/nj5BQ/Aww==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.4.7", + "@vue/shared": "3.4.7" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.1.tgz", + "integrity": "sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA==", + "dev": true + }, + "node_modules/@vue/reactivity": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.7.tgz", + "integrity": "sha512-F539DO0ogH0+L8F9Pnw7cjqibcmSOh5UTk16u5f4MKQ8fraqepI9zdh+sozPX6VmEHOcjo8qw3Or9ZcFFw4SZA==", + "dev": true, + "dependencies": { + "@vue/shared": "3.4.7" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.7.tgz", + "integrity": "sha512-QMMsWRQaD3BpGyjjChthpl4Mji4Fjx1qfdufsXlDkKU3HV+hWNor2z+29F+E1MmVcP0ZfRZUfqYgtsQoL7IGwQ==", + "dev": true, + "dependencies": { + "@vue/reactivity": "3.4.7", + "@vue/shared": "3.4.7" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.7.tgz", + "integrity": "sha512-XwegyUY1rw8zxsX1Z36vwYcqo+uOgih5ti7y9vx+pPFhNdSQmN4LqK2RmSeAJG1oKV8NqSUmjpv92f/x6h0SeQ==", + "dev": true, + "dependencies": { + "@vue/runtime-core": "3.4.7", + "@vue/shared": "3.4.7", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.7.tgz", + "integrity": "sha512-3bWnYLEkLLhkDWqvNk7IvbQD4UcxvFKxELBiOO2iG3m6AniFIsBWfHOO5tLVQnjdWkODu4rq0GipmfEenVAK5Q==", + "dev": true, + "dependencies": { + "@vue/compiler-ssr": "3.4.7", + "@vue/shared": "3.4.7" + }, + "peerDependencies": { + "vue": "3.4.7" + } + }, + "node_modules/@vue/shared": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.7.tgz", + "integrity": "sha512-G+i4glX1dMJk88sbJEcQEGWRQnVm9eIY7CcQbO5dpdsD9SF8jka3Mr5OqZYGjczGN1+D6EUwdu6phcmcx9iuPA==", + "dev": true + }, + "node_modules/@vueuse/core": { + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.7.1.tgz", + "integrity": "sha512-74mWHlaesJSWGp1ihg76vAnfVq9NTv1YT0SYhAQ6zwFNdBkkP+CKKJmVOEHcdSnLXCXYiL5e7MaewblfiYLP7g==", + "dev": true, + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.7.1", + "@vueuse/shared": "10.7.1", + "vue-demi": ">=0.14.6" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.6", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz", + "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/integrations": { + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-10.7.1.tgz", + "integrity": "sha512-cKo5LEeKVHdBRBtMTOrDPdR0YNtrmN9IBfdcnY2P3m5LHVrsD0xiHUtAH1WKjHQRIErZG6rJUa6GA4tWZt89Og==", + "dev": true, + "dependencies": { + "@vueuse/core": "10.7.1", + "@vueuse/shared": "10.7.1", + "vue-demi": ">=0.14.6" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "*", + "axios": "*", + "change-case": "*", + "drauu": "*", + "focus-trap": "*", + "fuse.js": "*", + "idb-keyval": "*", + "jwt-decode": "*", + "nprogress": "*", + "qrcode": "*", + "sortablejs": "*", + "universal-cookie": "*" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/integrations/node_modules/vue-demi": { + "version": "0.14.6", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz", + "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.7.1.tgz", + "integrity": "sha512-jX8MbX5UX067DYVsbtrmKn6eG6KMcXxLRLlurGkZku5ZYT3vxgBjui2zajvUZ18QLIjrgBkFRsu7CqTAg18QFw==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.7.1.tgz", + "integrity": "sha512-v0jbRR31LSgRY/C5i5X279A/WQjD6/JsMzGa+eqt658oJ75IvQXAeONmwvEMrvJQKnRElq/frzBR7fhmWY5uLw==", + "dev": true, + "dependencies": { + "vue-demi": ">=0.14.6" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.6", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz", + "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/algoliasearch": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.22.1.tgz", + "integrity": "sha512-jwydKFQJKIx9kIZ8Jm44SdpigFwRGPESaxZBaHSV0XWN2yBJAOT4mT7ppvlrpA4UGzz92pqFnVKr/kaZXrcreg==", + "dev": true, + "dependencies": { + "@algolia/cache-browser-local-storage": "4.22.1", + "@algolia/cache-common": "4.22.1", + "@algolia/cache-in-memory": "4.22.1", + "@algolia/client-account": "4.22.1", + "@algolia/client-analytics": "4.22.1", + "@algolia/client-common": "4.22.1", + "@algolia/client-personalization": "4.22.1", + "@algolia/client-search": "4.22.1", + "@algolia/logger-common": "4.22.1", + "@algolia/logger-console": "4.22.1", + "@algolia/requester-browser-xhr": "4.22.1", + "@algolia/requester-common": "4.22.1", + "@algolia/requester-node-http": "4.22.1", + "@algolia/transporter": "4.22.1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz", + "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.11", + "@esbuild/android-arm": "0.19.11", + "@esbuild/android-arm64": "0.19.11", + "@esbuild/android-x64": "0.19.11", + "@esbuild/darwin-arm64": "0.19.11", + "@esbuild/darwin-x64": "0.19.11", + "@esbuild/freebsd-arm64": "0.19.11", + "@esbuild/freebsd-x64": "0.19.11", + "@esbuild/linux-arm": "0.19.11", + "@esbuild/linux-arm64": "0.19.11", + "@esbuild/linux-ia32": "0.19.11", + "@esbuild/linux-loong64": "0.19.11", + "@esbuild/linux-mips64el": "0.19.11", + "@esbuild/linux-ppc64": "0.19.11", + "@esbuild/linux-riscv64": "0.19.11", + "@esbuild/linux-s390x": "0.19.11", + "@esbuild/linux-x64": "0.19.11", + "@esbuild/netbsd-x64": "0.19.11", + "@esbuild/openbsd-x64": "0.19.11", + "@esbuild/sunos-x64": "0.19.11", + "@esbuild/win32-arm64": "0.19.11", + "@esbuild/win32-ia32": "0.19.11", + "@esbuild/win32-x64": "0.19.11" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/focus-trap": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", + "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", + "dev": true, + "dependencies": { + "tabbable": "^6.2.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true + }, + "node_modules/markdown-it": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.0.0.tgz", + "integrity": "sha512-seFjF0FIcPt4P9U39Bq1JYblX0KZCjDLFFQPHpL5AzHpqPEKtosxmdq/LTVZnjfH7tjt9BxStm+wXcDBNuYmzw==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.0.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true + }, + "node_modules/minisearch": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-6.3.0.tgz", + "integrity": "sha512-ihFnidEeU8iXzcVHy74dhkxh/dn8Dc08ERl0xwoMMGqp4+LvRSCgicb+zGqWthVokQKvCSxITlh3P08OzdTYCQ==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.4.33", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", + "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.19.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.3.tgz", + "integrity": "sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/rollup": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.4.tgz", + "integrity": "sha512-2ztU7pY/lrQyXSCnnoU4ICjT/tCG9cdH3/G25ERqE3Lst6vl2BCM5hL2Nw+sslAvAf+ccKsAq1SkKQALyqhR7g==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.9.4", + "@rollup/rollup-android-arm64": "4.9.4", + "@rollup/rollup-darwin-arm64": "4.9.4", + "@rollup/rollup-darwin-x64": "4.9.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.9.4", + "@rollup/rollup-linux-arm64-gnu": "4.9.4", + "@rollup/rollup-linux-arm64-musl": "4.9.4", + "@rollup/rollup-linux-riscv64-gnu": "4.9.4", + "@rollup/rollup-linux-x64-gnu": "4.9.4", + "@rollup/rollup-linux-x64-musl": "4.9.4", + "@rollup/rollup-win32-arm64-msvc": "4.9.4", + "@rollup/rollup-win32-ia32-msvc": "4.9.4", + "@rollup/rollup-win32-x64-msvc": "4.9.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/search-insights": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.13.0.tgz", + "integrity": "sha512-Orrsjf9trHHxFRuo9/rzm0KIWmgzE8RMlZMzuhZOJ01Rnz3D0YBAe+V6473t6/H6c7irs6Lt48brULAiRWb3Vw==", + "dev": true, + "peer": true + }, + "node_modules/shikiji": { + "version": "0.9.18", + "resolved": "https://registry.npmjs.org/shikiji/-/shikiji-0.9.18.tgz", + "integrity": "sha512-/tFMIdV7UQklzN13VjF0/XFzmii6C606Jc878hNezvB8ZR8FG8FW9j0I4J9EJre0owlnPntgLVPpHqy27Gs+DQ==", + "dev": true, + "dependencies": { + "shikiji-core": "0.9.18" + } + }, + "node_modules/shikiji-core": { + "version": "0.9.18", + "resolved": "https://registry.npmjs.org/shikiji-core/-/shikiji-core-0.9.18.tgz", + "integrity": "sha512-PKTXptbrp/WEDjNHV8OFG9KkfhmR0pSd161kzlDDlgQ0HXAnqJYNDSjqsy1CYZMx5bSvLMy42yJj9oFTqmkNTQ==", + "dev": true + }, + "node_modules/shikiji-transformers": { + "version": "0.9.18", + "resolved": "https://registry.npmjs.org/shikiji-transformers/-/shikiji-transformers-0.9.18.tgz", + "integrity": "sha512-lvKVfgx1ETDqUNxqiUn+whlnjQiunsAg76DOpzjjxkHE/bLcwa+jrghcMxQhui86SLR1tzCdM4Imh+RxW0LI2Q==", + "dev": true, + "dependencies": { + "shikiji": "0.9.18" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "dev": true + }, + "node_modules/uc.micro": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.0.0.tgz", + "integrity": "sha512-DffL94LsNOccVn4hyfRe5rdKa273swqeA5DJpMOeFmEn1wCDc7nAbbB0gXlgBCL7TNzeTv6G7XVWzan7iJtfig==", + "dev": true + }, + "node_modules/vite": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.11.tgz", + "integrity": "sha512-XBMnDjZcNAw/G1gEiskiM1v6yzM4GE5aMGvhWTlHAYYhxb7S3/V1s3m2LDHa8Vh6yIWYYB0iJwsEaS523c4oYA==", + "dev": true, + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.32", + "rollup": "^4.2.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "1.0.0-rc.36", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.0.0-rc.36.tgz", + "integrity": "sha512-2z4dpM9PplN/yvTifhavOIAazlCR6OJ5PvLoRbc+7LdcFeIlCsuDGENLX4HjMW18jQZF5/j7++PNqdBfeazxUA==", + "dev": true, + "dependencies": { + "@docsearch/css": "^3.5.2", + "@docsearch/js": "^3.5.2", + "@types/markdown-it": "^13.0.7", + "@vitejs/plugin-vue": "^5.0.2", + "@vue/devtools-api": "^6.5.1", + "@vueuse/core": "^10.7.1", + "@vueuse/integrations": "^10.7.1", + "focus-trap": "^7.5.4", + "mark.js": "8.11.1", + "minisearch": "^6.3.0", + "shikiji": "^0.9.17", + "shikiji-core": "^0.9.17", + "shikiji-transformers": "^0.9.17", + "vite": "^5.0.11", + "vue": "^3.4.5" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4.3.2", + "postcss": "^8.4.33" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vitepress-plugin-tabs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/vitepress-plugin-tabs/-/vitepress-plugin-tabs-0.5.0.tgz", + "integrity": "sha512-SIhFWwGsUkTByfc2b279ray/E0Jt8vDTsM1LiHxmCOBAEMmvzIBZSuYYT1DpdDTiS3SuJieBheJkYnwCq/yD9A==", + "dev": true, + "peerDependencies": { + "vitepress": "^1.0.0-rc.27", + "vue": "^3.3.8" + } + }, + "node_modules/vue": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.7.tgz", + "integrity": "sha512-4urmkWpudekq0CPNMO7p6mBGa9qmTXwJMO2r6CT4EzIJVG7WoSReiysiNb7OSi/WI113oX0Srn9Rz1k/DCXKFQ==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.4.7", + "@vue/compiler-sfc": "3.4.7", + "@vue/runtime-dom": "3.4.7", + "@vue/server-renderer": "3.4.7", + "@vue/shared": "3.4.7" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..eea8fa08 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,12 @@ +{ + "scripts": { + "docs:dev": "vitepress dev", + "docs:build": "vitepress build", + "docs:preview": "vitepress preview" + }, + "devDependencies": { + "markdown-it": "^14.0.0", + "vitepress": "^1.0.0-rc.36", + "vitepress-plugin-tabs": "^0.5.0" + } +} diff --git a/docs/reference/actions/index.yml b/docs/reference/actions/index.yml deleted file mode 100755 index 678015b1..00000000 --- a/docs/reference/actions/index.yml +++ /dev/null @@ -1,2 +0,0 @@ -expanded: false -order: c diff --git a/docs/reference/actions/types.md b/docs/reference/actions/types.md deleted file mode 100755 index 5eae1498..00000000 --- a/docs/reference/actions/types.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -label: Available types ---- - -# Available action types - -The following action types are natively available in the bundle: - -- Common actions - - [Link](types/link.md) - - [Button](types/button.md) - - [Form](types/form.md) -- Base actions - - [Action](types/action.md) \ No newline at end of file diff --git a/docs/reference/actions/types/action.md b/docs/reference/actions/types/action.md deleted file mode 100755 index 43363046..00000000 --- a/docs/reference/actions/types/action.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -label: Action -order: z -tags: - - actions ---- - -# Action type - -The `ActionType` represents a base action, used as a parent for every other action type in the bundle. - -+-------------+---------------------------------------------------------------------+ -| Parent type | none -+-------------+---------------------------------------------------------------------+ -| Class | [:icon-mark-github: ActionType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/ActionType.php) -+-------------+---------------------------------------------------------------------+ - -## Options - -{{ include '_action_options' }} diff --git a/docs/reference/actions/types/link.md b/docs/reference/actions/types/link.md deleted file mode 100755 index 4f286965..00000000 --- a/docs/reference/actions/types/link.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -label: Link -order: b -tags: - - actions ---- - -# Link action type - -The `LinkActionType` represents an action displayed as a link. - -+-------------+---------------------------------------------------------------------+ -| Parent type | [ActionType](action.md) -+-------------+---------------------------------------------------------------------+ -| Class | [:icon-mark-github: LinkActionType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/LinkActionType.php) -+-------------+---------------------------------------------------------------------+ - -## Options - -### `href` - -- **type**: `string` or `callable` -- **default**: `'#'` - -A value used as an action link [href attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-href). - -```php # -use Kreyu\Bundle\DataTableBundle\Action\Type\LinkActionType; - -$builder - ->addAction('back', LinkActionType::class, [ - 'href' => $this->urlGenerator->generate('category_index'), - ]) -; -``` - -!!! Note -The action confirmation configuration inherits value of this option as its `href`. -!!! - -### `target` - -- **type**: `string` or `callable` -- **default**: `'_self'` - -Sets the value that will be used as an anchor [target attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target). - -```php # -use Kreyu\Bundle\DataTableBundle\Action\Type\LinkActionType; - -$builder - ->addAction('wiki', LinkActionType::class, [ - 'target' => '_blank', - ]) -; -``` - -## Inherited options - -{{ include '_action_options' }} diff --git a/docs/reference/columns/index.yml b/docs/reference/columns/index.yml deleted file mode 100755 index 568aa35b..00000000 --- a/docs/reference/columns/index.yml +++ /dev/null @@ -1,2 +0,0 @@ -expanded: false -order: a diff --git a/docs/reference/columns/types.md b/docs/reference/columns/types.md deleted file mode 100755 index 6c34ba24..00000000 --- a/docs/reference/columns/types.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -label: Available types ---- - -# Available column types - -The following column types are natively available in the bundle: - -- Text columns - - [Text](types/text) - - [Number](types/number) - - [Money](types/money) - - [Boolean](types/boolean) - - [Link](types/link) -- Date and time columns - - [DateTime](types/date-time) - - [DatePeriod](types/date-period) -- Special columns - - [Collection](types/collection) - - [Form](types/form) - - [Template](types/template) - - [Actions](types/actions) - - [Checkbox](types/checkbox) -- Base columns - - [Column](types/column) \ No newline at end of file diff --git a/docs/reference/columns/types/actions.md b/docs/reference/columns/types/actions.md deleted file mode 100755 index bb637d1b..00000000 --- a/docs/reference/columns/types/actions.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -label: Actions -order: j -tags: - - columns ---- - -# Actions column type - -The `ActionsColumnType` represents a column that contains row actions. - -+-------------+---------------------------------------------------------------------+ -| Parent type | [ColumnType](column) -+-------------+---------------------------------------------------------------------+ -| Class | [:icon-mark-github: ActionsColumnType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/ActionsColumnType.php) -+-------------+---------------------------------------------------------------------+ - -## Options - -### `actions` - -- **type**: `array` -- **default**: `[]` - -This option contains a list of actions. Each action consists of _three_ options: - -::: -- `type` - - **type**: `string` - Fully qualified class name of the [action type](../../../reference/actions/types.md). -::: -::: -- `type_options` - - **type**: `array` - - **default**: `[]` - Options passed to the action type. -::: -::: -- `visible` - - **type**: `bool`or `callable` - - **default**: `true` - Determines whether the action should be visible. -::: - -Example usage: - -```php # -use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; -use Kreyu\Bundle\DataTableBundle\Column\Type\ActionsColumnType; - -$builder - ->addColumn('actions', ActionsColumnType::class, [ - 'actions' => [ - 'show' => [ - 'type' => ButtonActionType::class, - 'type_options' => [ - 'url' => function (Product $product): string { - return $this->urlGenerator->generate('category_show', [ - 'id' => $product->getId(), - ]); - }), - ], - 'visible' => function (Product $product): bool { - return $product->isActive(); - } - ], - ], - ]) -; -``` - -## Inherited options - -{{ include '_column_options' }} diff --git a/docs/reference/columns/types/boolean.md b/docs/reference/columns/types/boolean.md deleted file mode 100755 index 5cee4bad..00000000 --- a/docs/reference/columns/types/boolean.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -label: Boolean -order: d -tags: - - columns ---- - -# Boolean column type - -The `BooleanColumnType` represents a column with value displayed as a "yes" or "no". - -+-------------+---------------------------------------------------------------------+ -| Parent type | [ColumnType](column) -+-------------+---------------------------------------------------------------------+ -| Class | [:icon-mark-github: BooleanColumnType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/BooleanColumnType.php) -+-------------+---------------------------------------------------------------------+ - -## Options - -### `label_true` - -- **type**: `string` or `Symfony\Component\Translation\TranslatableInterface` -- **default**: `'Yes'` - -Sets the value that will be displayed if value is truthy. - -### `label_false` - -- **type**: `string` or `Symfony\Component\Translation\TranslatableInterface` -- **default**: `'No'` - -Sets the value that will be displayed if row value is falsy. - -## Inherited options - -{{ include '_column_options' }} diff --git a/docs/reference/columns/types/checkbox.md b/docs/reference/columns/types/checkbox.md deleted file mode 100755 index 3849d798..00000000 --- a/docs/reference/columns/types/checkbox.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -label: Checkbox -order: k -tags: - - columns ---- - -# Checkbox column type - -The `CheckboxColumnType` represents a column with checkboxes, both in header and value rows. - -This column is used for [batch actions](../../../features/actions/batch-actions.md) to let user select the desired rows. - -+-------------+---------------------------------------------------------------------+ -| Parent type | [ColumnType](column) -+-------------+---------------------------------------------------------------------+ -| Class | [:icon-mark-github: CheckboxColumnType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/CheckboxColumnType.php) -+-------------+---------------------------------------------------------------------+ - -## Options - -### `identifier_name` - -- **type**: `string` -- **default**: `'id'` - -A name of the property to use in the batch actions. - -For more details about this option's influence on the batch actions, see ["Changing the identifier parameter name"](../../../features/actions/batch-actions.md#changing-the-identifier-parameter-name) section. - -## Inherited options - -{{ option_label_default_value = '`\'□\'`' }} -{{ option_property_path_default_value = '`\'id\'`' }} - -{{ include '_column_options' }} diff --git a/docs/reference/columns/types/column.md b/docs/reference/columns/types/column.md deleted file mode 100755 index 3087739a..00000000 --- a/docs/reference/columns/types/column.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -label: Column -order: z -tags: - - columns ---- - -# Column type - -The `ColumnType` represents a base column, used as a parent for every other column type in the bundle. - -+-------------+---------------------------------------------------------------------+ -| Parent type | none -+-------------+---------------------------------------------------------------------+ -| Class | [:icon-mark-github: ColumnType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/ColumnType.php) -+-------------+---------------------------------------------------------------------+ - -## Options - -{{ include '_column_options' }} diff --git a/docs/reference/columns/types/date-period.md b/docs/reference/columns/types/date-period.md deleted file mode 100755 index 5c4b22b5..00000000 --- a/docs/reference/columns/types/date-period.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -label: DatePeriod -order: f -tags: - - columns ---- - -# DatePeriod column type - -The `DatePeriodColumnType` represents a column with value displayed as a date (and with time by default). - -+-------------+---------------------------------------------------------------------+ -| Parent type | [ColumnType](column) -+-------------+---------------------------------------------------------------------+ -| Class | [:icon-mark-github: DatePeriodColumnType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/DatePeriodColumnType.php) -+-------------+---------------------------------------------------------------------+ - -## Options - -### `format` - -- **type**: `string` -- **default**: `'d.m.Y H:i:s'` - -The format specifier is the same as supported by [date](https://www.php.net/date). - -### `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-time.md b/docs/reference/columns/types/date-time.md deleted file mode 100755 index 8af05801..00000000 --- a/docs/reference/columns/types/date-time.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -label: DateTime -order: e -tags: - - columns ---- - -# DateTime column type - -The `DateTimeColumnType` represents a column with value displayed as a date and time. - -+-------------+---------------------------------------------------------------------+ -| Parent type | [ColumnType](column) -+-------------+---------------------------------------------------------------------+ -| Class | [:icon-mark-github: DateTimeColumnType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/DateTimeColumnType.php) -+-------------+---------------------------------------------------------------------+ - -## Options - -### `format` - -- **type**: `string` -- **default**: `'d.m.Y H:i:s'` - -The format specifier is the same as supported by [date](https://www.php.net/date). - -## Inherited options - -{{ include '_column_options' }} diff --git a/docs/reference/columns/types/date.md b/docs/reference/columns/types/date.md deleted file mode 100644 index 77ecda35..00000000 --- a/docs/reference/columns/types/date.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -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 deleted file mode 100755 index cd802d1a..00000000 --- a/docs/reference/columns/types/form.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -label: Form -order: h -tags: - - columns ---- - -# Form column type - -The `FormColumnType` represents a column with value displayed as a form input. - -+-------------+---------------------------------------------------------------------+ -| Parent type | [ColumnType](column) -+-------------+---------------------------------------------------------------------+ -| Class | [:icon-mark-github: FormColumnType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/FormColumnType.php) -+-------------+---------------------------------------------------------------------+ - -## Options - -### `form` - -- **type**: `string` -- **default**: `'Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType'` - -An instance of form that contains a collection of fields to render in the column. - -### `entry_options` - -- **type**: `false`, `null` or `string` -- **default**: `null` - the child path is "guessed" from the column name - -This is the path to the child form of each collection field. For example, if you have a collection of `ProductType` -which contains `name` and `quantity` fields, and you want to display the quantity field on the column, this option value should equal `quantity`. - -Setting this option to `false` disables this functionality and renders the form directly. - -## Inherited options - -{{ include '_column_options' }} diff --git a/docs/reference/columns/types/template.md b/docs/reference/columns/types/template.md deleted file mode 100755 index e3293c69..00000000 --- a/docs/reference/columns/types/template.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -label: Template -order: i -tags: - - columns ---- - -# Template column type - -The `TemplateColumnType` represents a column with value displayed as a Twig template. - -+-------------+---------------------------------------------------------------------+ -| Parent type | [ColumnType](column) -+-------------+---------------------------------------------------------------------+ -| Class | [:icon-mark-github: TemplateColumnType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/TemplateColumnType.php) -+-------------+---------------------------------------------------------------------+ - -## Options - -### `template_path` - -- **type**: `string` or `callable` - -Sets the path to the template that should be rendered. - -### `template_vars` - -- **type**: `string` or `callable` - -Sets the variables used within the template. - -## Inherited options - -{{ include '_column_options' }} diff --git a/docs/reference/columns/types/text.md b/docs/reference/columns/types/text.md deleted file mode 100755 index 22613648..00000000 --- a/docs/reference/columns/types/text.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -label: Text -order: a -tags: - - columns ---- - -# Text column type - -The `TextColumnType` represents a column with value displayed as a text. - -+-------------+---------------------------------------------------------------------+ -| Parent type | [ColumnType](column) -+-------------+---------------------------------------------------------------------+ -| Class | [:icon-mark-github: TextColumnType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/TextColumnType.php) -+-------------+---------------------------------------------------------------------+ - -## Options - -This column type has no additional options. - -## Inherited options - -{{ include '_column_options' }} diff --git a/docs/reference/exporters/index.yml b/docs/reference/exporters/index.yml deleted file mode 100755 index 4389e411..00000000 --- a/docs/reference/exporters/index.yml +++ /dev/null @@ -1,2 +0,0 @@ -expanded: false -order: d diff --git a/docs/reference/exporters/types.md b/docs/reference/exporters/types.md deleted file mode 100755 index 90a8fd78..00000000 --- a/docs/reference/exporters/types.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -label: Available types ---- - -# Available exporter 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) -- Base exporters - - [Exporter](types/exporter.md) diff --git a/docs/reference/exporters/types/exporter.md b/docs/reference/exporters/types/exporter.md deleted file mode 100755 index 3af1b869..00000000 --- a/docs/reference/exporters/types/exporter.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -label: Exporter -visibility: hidden ---- - -# Exporter type - -The `ExporterType` represents a base exporter, used as a parent for every other exporter type in the bundle. - -+---------------------+--------------------------------------------------------------+ -| Parent type | none -+---------------------+--------------------------------------------------------------+ -| Class | [:icon-mark-github: ExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/ExporterType.php) -+---------------------+--------------------------------------------------------------+ - -## Options - -{{ include '_exporter_options' }} diff --git a/docs/reference/exporters/types/open-spout/_open-spout_options.md b/docs/reference/exporters/types/open-spout/_open-spout_options.md deleted file mode 100644 index 4a1f7dfd..00000000 --- a/docs/reference/exporters/types/open-spout/_open-spout_options.md +++ /dev/null @@ -1,101 +0,0 @@ -### `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 deleted file mode 100755 index 64e2a716..00000000 --- a/docs/reference/exporters/types/open-spout/csv.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -label: CSV -order: a -tags: - - exporters - - openspout ---- - -# OpenSpout CSV exporter type - -The `CsvExporterType` represents an exporter that uses an [OpenSpout](https://github.com/openspout/openspout) CSV writer. - -+---------------------+--------------------------------------------------------------+ -| 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) -+---------------------+--------------------------------------------------------------+ - -## Options - -### `field_delimiter` - -- **type**: `string` -- **default**: `','` - -Represents a string that separates the values. - -### `field_enclosure` - -- **type**: `string` -- **default**: `'"'` - -Represents a string that wraps the values. - -### `should_add_bom` - -- **type**: `bool` -- **default**: `true` - -Determines whether a BOM character should be added at the beginning of the file. - -### `flush_threshold` - -- **type**: `int` -- **default**: `500` - -Represents a number of rows after which the output should be flushed to a file. - -## Inherited options - -{{ 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 deleted file mode 100755 index 378e4def..00000000 --- a/docs/reference/exporters/types/open-spout/index.yml +++ /dev/null @@ -1 +0,0 @@ -label: OpenSpout \ No newline at end of file diff --git a/docs/reference/exporters/types/open-spout/ods.md b/docs/reference/exporters/types/open-spout/ods.md deleted file mode 100755 index 7f9f9247..00000000 --- a/docs/reference/exporters/types/open-spout/ods.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -label: ODS -order: c -tags: - - exporters - - openspout ---- - -# OpenSpout ODS exporter type - -The `OdsExporterType` represents an exporter that uses an [OpenSpout](https://github.com/openspout/openspout) ODS writer. - -+---------------------+--------------------------------------------------------------+ -| 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) -+---------------------+--------------------------------------------------------------+ - -## Options - -### `default_row_style` - -- **type**: `OpenSpout\Common\Entity\Style\Style` -- **default**: an unmodified instance of `Style` class - -An instance of style class that will be applied to all rows. - -### `should_create_new_sheets_automatically` - -- **type**: `bool` -- **default**: `true` - -Determines whether new sheets should be created automatically -when the maximum number of rows (1,048,576) per sheet is reached. - -### `default_column_width` - -- **type**: `null` or `float` -- **default**: `null` - -Represents a width that will be applied to all columns by default. - -### `default_row_height` - -- **type**: `null` or `float` -- **default**: `null` - -Represents a height that will be applied to all rows by default. - -## Inherited options - -{{ 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 deleted file mode 100644 index f736d2a6..00000000 --- a/docs/reference/exporters/types/open-spout/open-spout.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -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-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/php-spreadsheet/html.md b/docs/reference/exporters/types/php-spreadsheet/html.md deleted file mode 100755 index 83a0090a..00000000 --- a/docs/reference/exporters/types/php-spreadsheet/html.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -label: HTML -order: d ---- - -# HTML exporter type - -The `HtmlExporterType` represents an exporter that uses a [PhpSpreadsheet Html writer](https://phpspreadsheet.readthedocs.io/en/latest/topics/reading-and-writing-to-file/#phpofficephpspreadsheetwriterhtml7). - -+---------------------+--------------------------------------------------------------+ -| Parent type | [PhpSpreadsheetType](php-spreadsheet.md) -+---------------------+--------------------------------------------------------------+ -| Class | [:icon-mark-github: HtmlExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/HtmlExporterType.php) -+---------------------+--------------------------------------------------------------+ - -## Options - -{{ include '_html_options.md' }} - -## Inherited options - -{{ include '_php-spreadsheet_options.md' }} diff --git a/docs/reference/exporters/types/php-spreadsheet/index.yml b/docs/reference/exporters/types/php-spreadsheet/index.yml deleted file mode 100755 index c06d8f95..00000000 --- a/docs/reference/exporters/types/php-spreadsheet/index.yml +++ /dev/null @@ -1 +0,0 @@ -label: PhpSpreadsheet \ No newline at end of file diff --git a/docs/reference/exporters/types/php-spreadsheet/ods.md b/docs/reference/exporters/types/php-spreadsheet/ods.md deleted file mode 100755 index 93d90efc..00000000 --- a/docs/reference/exporters/types/php-spreadsheet/ods.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -label: ODS -order: e ---- - -# ODS exporter type - -The `OdsExporterType` represents an exporter that uses a [PhpSpreadsheet Ods writer](https://phpspreadsheet.readthedocs.io/en/latest/topics/reading-and-writing-to-file/#phpofficephpspreadsheetwriterhtml7). - -+---------------------+--------------------------------------------------------------+ -| Parent type | [PhpSpreadsheetType](php-spreadsheet.md) -+---------------------+--------------------------------------------------------------+ -| Class | [:icon-mark-github: OdsExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/OdsExporterType.php) -+---------------------+--------------------------------------------------------------+ - -## Options - -This exporter type has no additional options. - -## Inherited options - -{{ include '_php-spreadsheet_options.md' }} diff --git a/docs/reference/exporters/types/php-spreadsheet/php-spreadsheet.md b/docs/reference/exporters/types/php-spreadsheet/php-spreadsheet.md deleted file mode 100755 index d8146510..00000000 --- a/docs/reference/exporters/types/php-spreadsheet/php-spreadsheet.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -label: PhpSpreadsheet -order: f ---- - -# PhpSpreadsheet exporter type - -The `PhpSpreadsheetExporterType` represents a base exporter, used as a parent for every other PhpSpreadsheet-related exporter type in the bundle. - -+---------------------+--------------------------------------------------------------+ -| Parent type | [ExporterType](../exporter.md) -+---------------------+--------------------------------------------------------------+ -| Class | [:icon-mark-github: PhpSpreadsheetExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/PhpSpreadsheetExporterType.php) -+---------------------+--------------------------------------------------------------+ - -## Options - -{{ include '_php-spreadsheet_options' }} \ No newline at end of file diff --git a/docs/reference/exporters/types/php-spreadsheet/xls.md b/docs/reference/exporters/types/php-spreadsheet/xls.md deleted file mode 100755 index 472716ca..00000000 --- a/docs/reference/exporters/types/php-spreadsheet/xls.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -label: XLS -order: b ---- - -# XLS exporter type - -The `XlsExporterType` represents an exporter that uses a [PhpSpreadsheet Xls writer](https://phpspreadsheet.readthedocs.io/en/latest/topics/reading-and-writing-to-file/#phpofficephpspreadsheetwriterxls). - -+---------------------+--------------------------------------------------------------+ -| Parent type | [PhpSpreadsheetType](php-spreadsheet.md) -+---------------------+--------------------------------------------------------------+ -| Class | [:icon-mark-github: XlsExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/XlsExporterType.php) -+---------------------+--------------------------------------------------------------+ - -## Options - -This exporter type has no additional options. - -## Inherited options - -{{ include '_php-spreadsheet_options.md' }} diff --git a/docs/reference/exporters/types/php-spreadsheet/xlsx.md b/docs/reference/exporters/types/php-spreadsheet/xlsx.md deleted file mode 100755 index d420c060..00000000 --- a/docs/reference/exporters/types/php-spreadsheet/xlsx.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -label: XLSX -order: c ---- - -# XLSX exporter type - -The `XlsxExporterType` represents an exporter that uses a [PhpSpreadsheet Xlsx writer](https://phpspreadsheet.readthedocs.io/en/latest/topics/reading-and-writing-to-file/#phpofficephpspreadsheetwriterxlsx). - -+---------------------+--------------------------------------------------------------+ -| Parent type | [PhpSpreadsheetType](php-spreadsheet.md) -+---------------------+--------------------------------------------------------------+ -| Class | [:icon-mark-github: XlsxExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/XlsxExporterType.php) -+---------------------+--------------------------------------------------------------+ - -## Options - -### `office_2003_compatibility` - -- **type**: `bool` -- **default**: `false` - -Because of a bug in the Office2003 compatibility pack, there can be some small issues when opening -Xlsx spreadsheets (mostly related to formula calculation). You can enable Office2003 compatibility by setting this option to `true`. - -!!!warning Warning -Office2003 compatibility option should only be used when needed because it disables several Office2007 file format options, -resulting in a **lower-featured** Office2007 spreadsheet! -!!! - -## Inherited options - -{{ include '_php-spreadsheet_options.md' }} diff --git a/docs/reference/filters/index.yml b/docs/reference/filters/index.yml deleted file mode 100755 index 8027f2f4..00000000 --- a/docs/reference/filters/index.yml +++ /dev/null @@ -1,2 +0,0 @@ -expanded: false -order: b diff --git a/docs/reference/filters/types.md b/docs/reference/filters/types.md deleted file mode 100755 index 20ebf442..00000000 --- a/docs/reference/filters/types.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -label: Available types ---- - -# Available filter types - -The following filter types are natively available in the bundle: - -- Doctrine ORM filters - - [String](types/doctrine-orm/string.md) - - [Numeric](types/doctrine-orm/numeric.md) - - [Boolean](types/doctrine-orm/boolean.md) - - [Date](types/doctrine-orm/date.md) - - [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 deleted file mode 100755 index 00cc4684..00000000 --- a/docs/reference/filters/types/_filter_options.md +++ /dev/null @@ -1,104 +0,0 @@ -### `label` - -- **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. - -### `label_translation_parameters` - -- **type**: `array` -- **default**: `[]` - -Sets the parameters used when translating the `label` option. - -### `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` - -- **type**: `null` or `string` -- **default**: `null` the query path is "guessed" from the filter name - -Sets the path used in the proxy query to perform the filtering on. - -### `form_type` - -- **type**: `string` -- **default**: {{ option_form_type_default_value ?? '`\'Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType\'`' }} - -This is the form type used to render the filter value field. - -### `form_options` - -- **type**: `array` -- **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. - -{{ option_form_options_notes }} - -### `operator_form_type` - -- **type**: `string` -- **default**: `Kreyu\Bundle\DataTableBundle\Filter\Form\Type\OperatorType` - -This is the form type used to render the filter operator field. - -!!! -**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_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 deleted file mode 100644 index 23aea655..00000000 --- a/docs/reference/filters/types/doctrine-orm/_doctrine_orm_filter_options.md +++ /dev/null @@ -1,8 +0,0 @@ -### `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 deleted file mode 100755 index 86b39844..00000000 --- a/docs/reference/filters/types/doctrine-orm/boolean.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -label: Boolean -order: c -tags: - - filters - - doctrine orm ---- - -# Boolean filter type - -The `BooleanFilterType` represents a filter that operates on boolean values. - -+---------------------+--------------------------------------------------------------+ -| 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, NotEquals -+---------------------+--------------------------------------------------------------+ -| Default operator | Equals -+---------------------+--------------------------------------------------------------+ - -## Options - -This filter type has no additional options. - -## Inherited 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 deleted file mode 100755 index 980d409e..00000000 --- a/docs/reference/filters/types/doctrine-orm/callback.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -label: Callback -order: g -tags: - - filters - - doctrine orm ---- - -# Callback filter type - -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 | [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 -+---------------------+--------------------------------------------------------------+ -| Default operator | Equals -+---------------------+--------------------------------------------------------------+ - -## Options - -### `callback` - -**type**: `callable` - -Sets callable that operates on the query passed as a first argument: - -```php # -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, [ - 'callback' => function (DoctrineOrmProxyQuery $query, FilterData $data, FilterInterface $filter): 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()) - ; - } - ]) -``` - -## Inherited 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 deleted file mode 100755 index 454cf852..00000000 --- a/docs/reference/filters/types/doctrine-orm/date-time.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -label: DateTime -order: e -tags: - - filters - - doctrine orm ---- - -# DateTime filter type - -The `DateTimeFilterType` represents a filter that operates on datetime values. - -+---------------------+--------------------------------------------------------------+ -| 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, NotEquals, GreaterThan, GreaterThanEquals, LessThan, LessThanEquals -+---------------------+--------------------------------------------------------------+ -| Default operator | Equals -+---------------------+--------------------------------------------------------------+ - -## Options - -This filter type has no additional options. - -## Inherited 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 deleted file mode 100755 index dafcfcdd..00000000 --- a/docs/reference/filters/types/doctrine-orm/date.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -label: Date -order: d -tags: - - filters - - doctrine orm ---- - -# Date filter type - -The `DateFilterType` represents a filter that operates on date values. - -+---------------------+--------------------------------------------------------------+ -| 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, NotEquals, GreaterThan, GreaterThanEquals, LessThan, LessThanEquals -+---------------------+--------------------------------------------------------------+ -| Default operator | Equals -+---------------------+--------------------------------------------------------------+ - -## Options - -This filter type has no additional 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' }} -{{ 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 deleted file mode 100644 index 33a8a033..00000000 --- a/docs/reference/filters/types/doctrine-orm/doctrine-orm.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -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 deleted file mode 100755 index 89542af2..00000000 --- a/docs/reference/filters/types/doctrine-orm/entity.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -label: Entity -order: f -tags: - - filters - - doctrine orm ---- - -# Entity filter type - -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 | [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, NotEquals, Contains, NotContains -+---------------------+--------------------------------------------------------------+ -| Default operator | Equals -+---------------------+--------------------------------------------------------------+ - -## Options - -This filter type has no additional options. - -## Inherited 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 deleted file mode 100755 index 9d70b57a..00000000 --- a/docs/reference/filters/types/doctrine-orm/index.yml +++ /dev/null @@ -1 +0,0 @@ -label: Doctrine ORM \ No newline at end of file diff --git a/docs/reference/filters/types/doctrine-orm/numeric.md b/docs/reference/filters/types/doctrine-orm/numeric.md deleted file mode 100755 index 45101d8f..00000000 --- a/docs/reference/filters/types/doctrine-orm/numeric.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -label: Numeric -order: b -tags: - - filters - - doctrine orm ---- - -# Numeric filter type - -The `NumericFilterType` represents a filter that operates on numeric values. - -+---------------------+--------------------------------------------------------------+ -| 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, NotEquals, GreaterThan, GreaterThanEquals, LessThan, LessThanEquals -+---------------------+--------------------------------------------------------------+ -| Default operator | Equals -+---------------------+--------------------------------------------------------------+ - -## Options - -This filter type has no additional options. - -## Inherited 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 deleted file mode 100755 index bc2ffb06..00000000 --- a/docs/reference/filters/types/doctrine-orm/string.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -label: String -order: a -tags: - - filters - - doctrine orm ---- - -# String filter type - -The `StringFilterType` represents a filter that operates on string values. - -+---------------------+--------------------------------------------------------------+ -| 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, NotEquals, Contains, NotContains, StartsWith, EndsWith -+---------------------+--------------------------------------------------------------+ -| Default operator | Contains -+---------------------+--------------------------------------------------------------+ - -## Options - -This filter type has no additional options. - -## Inherited 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 deleted file mode 100755 index d99a375a..00000000 --- a/docs/reference/filters/types/filter.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -label: Filter -order: z -tags: - - filters ---- - -# Filter type - -The `FilterType` represents a base filter used as a parent for every other filter type in the bundle. - -+---------------------+--------------------------------------------------------------+ -| Parent type | none -+---------------------+--------------------------------------------------------------+ -| Class | [:icon-mark-github: FilterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/FilterType.php) -+---------------------+--------------------------------------------------------------+ -| Form Type | [TextType](https://symfony.com/doc/current/reference/forms/types/text.html) -+---------------------+--------------------------------------------------------------+ -| Supported operators | none -+---------------------+--------------------------------------------------------------+ - -## Options - -{{ include '_filter_options' }} diff --git a/docs/reference/filters/types/search.md b/docs/reference/filters/types/search.md deleted file mode 100644 index fa591e98..00000000 --- a/docs/reference/filters/types/search.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -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 deleted file mode 100755 index 57d95bac..00000000 --- a/docs/reference/index.yml +++ /dev/null @@ -1,2 +0,0 @@ -icon: gear -order: 1 \ No newline at end of file diff --git a/docs/retype.yml b/docs/retype.yml deleted file mode 100755 index 24979a93..00000000 --- a/docs/retype.yml +++ /dev/null @@ -1,29 +0,0 @@ -# Documentation powered by Retype -# https://retype.com/ - -input: . -output: .retype -url: https://data-table-bundle.swroblewski.pl/ -favicon: static/favicon.png -branding: - title: DataTableBundle - label: Docs -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." diff --git a/docs/src/docs/components/actions.md b/docs/src/docs/components/actions.md new file mode 100644 index 00000000..16d0bc09 --- /dev/null +++ b/docs/src/docs/components/actions.md @@ -0,0 +1,571 @@ +# Actions + +[[toc]] + +## Prerequisites + +There are three contexts that the action can be defined with: + +**Regular actions** + +Regular actions are not bound to any data, and are displayed on top of the data table. +This kind of action can be used, for example, for "Create" button that redirects to a form. + +**Row actions** + +Actions that are bound to a row, displayed in an "actions" column. +This kind of action can be used, for example, for "Update" button that redirects to edit form for a record. + +**Batch actions** + +Actions that are bound to a multiple rows, selected by a checkbox column. +Batch actions require `batch` Stimulus controller enabled: + +```json5 +// assets/controllers.json +{ + "controllers": { + "@kreyu/data-table-bundle": { + // ... + "batch": { + "enabled": true + } + } + } +} +``` + +## Adding actions + +Actions can be added by using a data table builder's `addAction()`, `addRowAction()` and `addBatchAction()` methods: + +```php +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; + +class ProductDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + $builder + ->addAction('create', ButtonActionType::class, [ + 'href' => $this->urlGenerator->generate('app_product_create'), + ]) + // note that row action has access to a row data in a callable + ->addRowAction('update', ButtonActionType::class, [ + 'href' => function (Product $product) { + return $this->urlGenerator->generate('app_product_update', [ + 'id' => $product->getId(), + ]); + } + ]) + ->addBatchAction('delete', ButtonActionType::class, [ + 'href' => $this->urlGenerator->generate('app_product_batch_delete'), + ]) + ; + } +} +``` + +Those methods accept _three_ arguments: + +- action name; +- action type — with a fully qualified class name; +- action options — defined by the action type, used to configure the action; + +For reference, see [available action types](../../reference/types/action.md). + +## Creating action types + +If built-in action types are not enough, you can create your own. In following chapters, we'll be creating an action that opens a modal. + +Action types are classes that implement [ActionTypeInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/ActionTypeInterface.php), although, it is recommended to extend from the [AbstractActionType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/AbstractActionType.php) class: + +```php +use Kreyu\Bundle\DataTableBundle\Action\Type\AbstractActionType; + +class ModalActionType extends AbstractActionType +{ +} +``` + +
+ +Recommended namespace for the action type classes is `App\DataTable\Action\Type\`. + +
+ +### Action type inheritance + +Because our modal action fundamentally renders as a button, let's base it off the built-in [`ButtonActionType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/ButtonActionType.php). +Provide the fully-qualified class name of the parent type in the `getParent()` method: + +```php +use Kreyu\Bundle\DataTableBundle\Action\Type\AbstractActionType; +use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; + +class ModalActionType extends AbstractActionType +{ + public function getParent(): ?string + { + return ButtonActionType::class; + } +} +``` + +::: tip +If you take a look at the [`AbstractActionType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/AbstractActionType.php), +you'll see that `getParent()` method returns fully-qualified name of the [`ActionType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/ActionType.php) type class. +This is the type that defines all the basic options, such as `attr`, `label`, etc. +::: + +### Rendering the action type + +Because our modal action is based off the built-in [`ButtonActionType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/ButtonActionType.php), +it will be rendered as a button without any additional configuration. However, in our case, we want to add the modal itself. + +First, create a custom theme for the data table, and create a `action_modal_value` block: + +```twig +{# templates/data_table/theme.html.twig #} + +{% block action_modal_value %} + + + +{% endblock %} +``` + +The block naming follows a set of rules: + +- for actions, it always starts with `action_` prefix; +- next comes the block prefix of the action type; +- last part is always the `_value` suffix; + +If you take a look at the [`AbstractActionType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/AbstractActionType.php), +you'll see that `getBlockPrefix()` returns snake cased short name of the type class, without the `ActionType` suffix. + +In our case, because the type class is named `ModalActionType`, the default block prefix equals `modal`. Simple as that. + +Now, the custom theme should be added to the bundle configuration: + +::: code-group + +```yaml [YAML] +kreyu_data_table: + defaults: + themes: + # ... + - 'data_table/theme.html.twig' +``` + +```php [PHP] +use Symfony\Config\KreyuDataTableConfig; + +return static function (KreyuDataTableConfig $config) { + $config->defaults()->themes([ + // ... + 'data_table/theme.html.twig', + ]); +}; +``` + +::: + +If the `action_modal_value` block wasn't defined in any of the configured themes, the bundle will render block of the parent type. +In our example, because we set [`ButtonActionType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/ButtonActionType.php) as a parent, a `action_button_value` block will be rendered. + +### Adding configuration options + +Action type options allow to configure the behavior of the action types. +The options are defined in the `configureOptions()` method, using the [OptionsResolver component](https://symfony.com/doc/current/components/options_resolver.html). + +Imagine, that you want to provide a template to render as the action modal contents. +The template could be provided by a custom `template_path` and `template_vars` options: + +```php +use Kreyu\Bundle\DataTableBundle\Action\Type\AbstractActionType; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class ModalActionType extends AbstractActionType +{ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + // define options required by the type + ->setRequired('template_path') + // define available options and their default values + ->setDefaults([ + 'template_vars' => [], + ]) + // optionally you can restrict type of the options + ->setAllowedTypes('template_path', 'string') + ->setAllowedTypes('template_vars', 'array') + ; + } +} +``` + +Now you can configure the new option when using the action type: + +```php +class UserDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + $builder + // ... + ->addRowAction('details', ModalActionType::class, [ + 'template_path' => 'user/details.html.twig', + ]) + ; + } +} +``` + +### Passing variables to the template + +Now, the `template_path` and `template_vars` options are defined, but are not utilized by the system in any way. +In our case, we'll pass the options to the view, and use them to render the template itself: + +```php +use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; +use Kreyu\Bundle\DataTableBundle\Action\ActionView; +use Kreyu\Bundle\DataTableBundle\Action\ActionInterface; + +class ModalActionType extends ButtonActionType +{ + public function buildView(ActionView $view, ActionInterface $action, array $options): void + { + $view->vars['template_path'] = $options['template_path']; + $view->vars['template_vars'] = $options['template_vars']; + } +} +``` + +Now we can update the template of the type class to use the newly added variables: + +```twig +{# templates/data_table/theme.html.twig #} + +{% block action_modal_value %} + + + +{% endblock %} +``` + +### Using row data in options + +> What if I want to pass an option based on the row data? + +If the action type is used for a row action, the `ActionView` parent will be an instance of `ColumnValueView`, +which can be used to retrieve a data of the row. This can be used in combination with accepting the `callable` options: + +```php +use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; +use Kreyu\Bundle\DataTableBundle\Action\ActionView; +use Kreyu\Bundle\DataTableBundle\Action\ActionInterface; +use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView; // [!code ++] + +class ModalActionType extends ButtonActionType +{ + public function buildView(ActionView $view, ActionInterface $action, array $options): void + { + if ($view->parent instanceof ColumnValueView) { // [!code ++] + $value = $view->parent->vars['value']; // [!code ++] + + foreach (['template_path', 'template_vars'] as $optionName) { // [!code ++] + if (is_callable($templatePath)) { // [!code ++] + $options[$optionName] = $options[$optionName]($value); // [!code ++] + } // [!code ++] + } // [!code ++] + } // [!code ++] + + $view->vars['template_path'] = $options['template_path']; + $view->vars['template_vars'] = $options['template_vars']; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + // define options required by the type + ->setRequired('template_path') + // define available options and their default values + ->setDefaults([ + 'template_vars' => [], + ]) + // optionally you can restrict type of the options + ->setAllowedTypes('template_path', 'string') // [!code --] + ->setAllowedTypes('template_path', ['string', 'callable']) // [!code ++] + ->setAllowedTypes('template_vars', 'array') // [!code --] + ->setAllowedTypes('template_vars', ['array', 'callable']) // [!code ++] + ; + } +} +``` + +Now, you can use the `callable` options when defining the modal row action: + +```php +class UserDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + $builder + // ... + ->addRowAction('details', ModalActionType::class, [ + 'template_path' => 'user/details.html.twig', + 'template_vars' => function (User $user) { // [!code ++] + return [ // [!code ++] + 'user_id' => $user->getId(), // [!code ++] + ]; // [!code ++] + }, // [!code ++] + ]) + ; + } +} +``` + +## Action type extensions + +Action type extensions allows modifying configuration of the existing action types, even the built-in ones. +Let's assume, that we want to add a [Bootstrap tooltip](https://getbootstrap.com/docs/5.3/components/tooltips/#overview) for every button action, as long as their `title` attribute is defined. + +Action type extensions are classes that implement [`ActionTypeExtensionInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Extension/ActionTypeExtensionInterface.php). +However, it's better to extend from the [`AbstractActionTypeExtension`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Extension/AbstractActionTypeExtension.php): + +```php +use Kreyu\Bundle\DataTableBundle\Action\Extension\AbstractActionTypeExtension; +use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class TooltipActionTypeExtension extends AbstractActionTypeExtension +{ + public function buildValueView(ActionValueView $view, ActionInterface $column, array $options): void + { + if (!$options['tooltip']) { + return; + } + + $title = $view->vars['attr']['title'] ?? null; + + if (empty($title)) { + return; + } + + $view->vars['attr']['data-bs-toggle'] = 'tooltip'; + $view->vars['attr']['data-bs-placement'] = 'top'; + $view->vars['attr']['data-bs-title'] = $title; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setDefault('tooltip', true) + ->setAllowedTypes('tooltip', 'bool') + ; + } + + public static function getExtendedTypes(): iterable + { + return [ButtonActionType::class]; + } +} +``` + +Now, as long as the button action `tooltip` option equals to `true` (by default), and a `title` attribute is set, +the action will be rendered with Bootstrap tooltip attributes. You can even use the action name instead of the `title` attribute! + +## Adding action confirmation + +Actions can be configured to require confirmation (by the user) before being executed. + +![Action confirmation modal with the Tabler theme](/action_confirmation_modal.png) + +To enable action confirmation, set its `confirmation` option to `true`: + +```php +$builder->addRowAction('remove', ButtonActionType::class, [ + 'confirmation' => true, +]); +``` + +To configure the confirmation modal, pass the array as the `confirmation` option: + +```php +$builder->addRowAction('remove', ButtonActionType::class, [ + 'confirmation' => [ + 'translation_domain' => 'KreyuDataTable', + 'label_title' => 'Action confirmation', + 'label_description' => 'Are you sure you want to execute this action?', + 'label_confirm' => 'Confirm', + 'label_cancel' => 'Cancel', + 'type' => 'danger', // "danger", "warning" or "info" + ], +]); +``` + +For reference, see details about the [`confirmation`](#) option. + +## Conditionally rendering the action + +Action visibility can be configured using its [`visible`](#) option: + +```php +$builder->addRowAction('remove', ButtonActionType::class, [ + 'visible' => $this->isGranted('ROLE_ADMIN'), +]); +``` + +Another approach would be simply not adding the action at all: + +```php +if ($this->isGranted('ROLE_ADMIN')) { + $builder->addRowAction('remove', ButtonActionType::class); +} +``` + +What differentiates those two methods, is that by using the `visible` option, the action is still defined in the data table, but is not rendered in the view. +It may be useful in some cases, for example, when the actions can be modified outside the data table builder. + +## Batch action specifics + +### Adding checkbox column + +Batch actions require the user to select specific rows. This is handled by the [`CheckboxColumnType`](../../reference/types/column/checkbox.md), +which simply renders a checkbox with value set to row identifier. To help with that process, +if at least one batch action is defined, this checkbox column will be added automatically. + +This column will be named `__batch`, which can be referenced using the constant: + +```php +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; + +$column = $builder->getColumn(DataTableBuilderInterface::BATCH_CHECKBOX_COLUMN_NAME); +``` + +This behavior can be disabled (or enabled back again) using the builder's method: + +```php +$builder->setAutoAddingBatchCheckboxColumn(false); +``` + +### Changing identifier parameter name + +By default, the checkbox column type will add the `id` parameter to the batch actions. +For example, checking rows with ID 1, 2 will result in: + +- every batch action's `href` parameter appended with `id[]=1&id[]=2` +- every batch action's `data-id` parameter set to `[1,2]` + +The parameter name can be changed by providing the `identifier_name` option: + +```php +use Kreyu\Bundle\DataTableBundle\Column\Type\CheckboxColumnType; + +$builder->addColumn('__batch', CheckboxColumnType::class, [ + 'identifier_name' => 'product_id', +]); +``` + +Using the above configuration, checking rows with ID 1, 2 will result in: + +- every batch action's `href` parameter appended with `product_id[]=1&product_id[]=2` +- every batch action's `data-product-id` parameter set to `[1,2]` + +If the action has no `href` parameter, the query parameters will not be appended. +The data parameters are not used internally and can be used for custom scripts. + +If `FormActionType` is used, the scripts will append hidden inputs with selected values, for example: + +```html + + +``` + +### Changing identifier parameter value + +By default, the checkbox column type will try to retrieve the identifier on the `id` property path. +This can be changed similarly to other column types, by providing the `property_path` option: + +```php +use Kreyu\Bundle\DataTableBundle\Column\Type\CheckboxColumnType; + +$builder->addColumn('__batch', CheckboxColumnType::class, [ + 'property_path' => 'uuid', +]); +``` + +If property accessor is not enough, use the `getter` option: + +```php +use Kreyu\Bundle\DataTableBundle\Column\Type\CheckboxColumnType; + +$builder->addColumn('__batch', CheckboxColumnType::class, [ + 'getter' => fn (Product $product) => $product->getUuid(), +]); +``` + +### Multiple checkbox columns + +Using multiple checkbox columns for a single data table is supported. +For example, using the following configuration: + +```php +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Kreyu\Bundle\DataTableBundle\Column\Type\CheckboxColumnType; + +class ProductDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + $builder + ->addColumn('productId', CheckboxColumnType::class, [ + 'property_path' => 'id', + 'identifier_name' => 'product_id', + ]) + ->addColumn('categoryId', CheckboxColumnType::class, [ + 'property_path' => 'category.id', + 'identifier_name' => 'category_id', + ]) + ; + } +} +``` + +And having a data set which consists of two rows: + +| Product ID | Category ID | +|------------|-------------| +| 1 | 3 | +| 2 | 4 | + +Checking the first row's product and second row's category will result in: + +- every batch action's `href` parameter appended with `product_id[]=1&category_id[]=4` +- every batch action's `data-product-id` parameter set to `[1]` and `data-category-id` set to `[4]` + +If the action has no `href` parameter, the query parameters will not be appended. +The data parameters are not used internally and can be used for custom scripts. + +If `FormActionType` is used, the scripts will append hidden inputs with selected values, for example: + +```html + + +``` \ No newline at end of file diff --git a/docs/src/docs/components/columns.md b/docs/src/docs/components/columns.md new file mode 100644 index 00000000..b1b29ec5 --- /dev/null +++ b/docs/src/docs/components/columns.md @@ -0,0 +1,276 @@ +# Columns + +[[toc]] + +## Adding columns + +To add a column, use the data table builder's `addColumn()` method: + +```php +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Kreyu\Bundle\DataTableBundle\Column\Type\NumberColumnType; +use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType; +use Kreyu\Bundle\DataTableBundle\Column\Type\DateTimeColumnType; + +class UserDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + $builder + ->addColumn('id', NumberColumnType::class) + ->addColumn('name', TextColumnType::class, [ + 'label' => 'Full name', + ]) + ->addColumn('createdAt', DateTimeColumnType::class, [ + 'format' => 'Y-m-d H:i:s', + ]) + ; + } +} +``` + +This method accepts _three_ arguments: + +- column name; +- column type — with a fully qualified class name; +- column options — defined by the column type, used to configure the column; + +For reference, see [available column types](../../reference/types/column.md). + +## Creating column types + +If [built-in column types](../../reference/types/column.md) are not enough, you can create your own. +In following chapters, we'll be creating a column that renders a phone number stored as an object: + +```php +readonly class PhoneNumber +{ + public function __construct( + public string $nationalNumber, + public string $countryCode, + ) +} +``` + +Column types are classes that implement [`ColumnTypeInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/ColumnTypeInterface.php), although, it's better to extend from the [`AbstractColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/AbstractColumnType.php): + +```php +use Kreyu\Bundle\DataTableBundle\Column\Type\AbstractColumnType; + +class PhoneNumberColumnType extends AbstractColumnType +{ +} +``` + +
+ +Recommended namespace for the column type classes is `App\DataTable\Column\Type\`. + +
+ +### Column type inheritance + +Because our phone number column fundamentally renders as a text, let's base it off the built-in [`TextColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/TextColumnType.php). +Provide the fully-qualified class name of the parent type in the `getParent()` method: + +```php +use Kreyu\Bundle\DataTableBundle\Column\Type\AbstractColumnType; +use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType; + +class PhoneNumberColumnType extends AbstractColumnType +{ + public function getParent(): ?string + { + return TextColumnType::class; + } +} +``` + +::: tip +If you take a look at the [`AbstractColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/AbstractColumnType.php), +you'll see that `getParent()` method returns fully-qualified name of the [`ColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/ColumnType.php) type class. +This is the type that defines all the basic options, such as `attr`, `label`, etc. +::: + +### Rendering the column type + +Because our phone number column is based off the built-in [`TextColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/TextColumnType.php), +it will be rendered as a text as long as the `PhoneNumber` object can be cast to string. However, in our case, let's store this logic in the template. + +First, create a custom theme for the data table, and create a `column_phone_number_value` block: + +```twig +{# templates/data_table/theme.html.twig #} + +{% block column_phone_number_value %} + +{{ value.countryCode }} {{ value.nationalNumber }} +{% endblock %} +``` + +The block naming follows a set of rules: + +- for columns, it always starts with `column` prefix; +- next comes the block prefix of the column type; +- last part of the block name represents a part of the column. The column is split into multiple parts when rendering: + - `label` - displayed in the column header and in [personalization](../features/personalization.md) column list; + - `header` - displayed at the top of the column, allows [sorting](../features/sorting.md) if the column is sortable; + - `value` - like shown in example above, it renders the value itself; + +If you take a look at the [`AbstractColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/AbstractColumnType.php), +you'll see that `getBlockPrefix()` returns snake cased short name of the type class, without the `ColumnType` suffix. + +In our case, because the type class is named `PhoneNumberColumnType`, the default block prefix equals `phone_number`. Simple as that. + +Now, the custom theme should be added to the bundle configuration: + +::: code-group + +```yaml [YAML] +kreyu_data_table: + defaults: + themes: + # ... + - 'data_table/theme.html.twig' +``` + +```php [PHP] +use Symfony\Config\KreyuDataTableConfig; + +return static function (KreyuDataTableConfig $config) { + $config->defaults()->themes([ + // ... + 'data_table/theme.html.twig', + ]); +}; +``` + +::: + + +If the `column_phone_number_value` block wasn't defined in any of the configured themes, the bundle will render block of the parent type. +In our example, because we set [`TextColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/TextColumnType.php) as a parent, a `column_phone_number_value` block will be rendered. + +### Adding configuration options + +Column type options allow to configure the behavior of the column types. +The options are defined in the `configureOptions()` method, using the [OptionsResolver component](https://symfony.com/doc/current/components/options_resolver.html). + +Imagine, that you want to determine whether the country code should be rendered. This could be achieved by using a `show_country_code` option: + +```php +use Kreyu\Bundle\DataTableBundle\Column\Type\AbstractColumnType; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class PhoneNumberColumnType extends AbstractColumnType +{ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + // define available options and their default values + ->setDefaults([ + 'show_country_code' => true, + ]) + // optionally you can restrict type of the options + ->setAllowedTypes('country_code', 'bool') + ; + } +} +``` + +Now you can configure the new option when using the column type: + +```php +class UserDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + $builder + // ... + ->addColumn('phone', PhoneNumberColumnType::class, [ + 'show_country_code' => false, + ]) + ; + } +} +``` + +### Passing variables to the template + +Now, the `show_country_code` option is defined, but is not utilized by the system in any way. +In our case, we'll pass the options to the view, and use them to render the template itself: + +```php +use Kreyu\Bundle\DataTableBundle\Column\Type\AbstractColumnType; +use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView; +use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface; + +class PhoneNumberColumnType extends AbstractColumnType +{ + public function buildValueView(ColumnValueView $view, ColumnInterface $column, array $options): void + { + $view->vars['show_country_code'] = $options['show_country_code']; + } +} +``` + +Now we can update the template of the type class to use the newly added variable: + +```twig +{# templates/data_table/theme.html.twig #} + +{% block column_phone_number_value %} + {% if show_country_code %} + +{{ value.countryCode }} + {% endif %} + + {{ value.nationalNumber }} +{% endblock %} +``` + +## Column type extensions + +Column type extensions allows modifying configuration of the existing column types, even the built-in ones. +Let's assume, that we want to add a `trim` option, which will automatically apply the PHP `trim` method +on every column type in the system that uses [`TextColumnType`](../../reference/types/column/text.md) as its parent. + +Column type extensions are classes that implement [`ColumnTypeExtensionInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Extension/ColumnTypeExtensionInterface.php). +However, it's better to extend from the [`AbstractColumnTypeExtension`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Extension/AbstractColumnTypeExtension.php): + +```php +use Kreyu\Bundle\DataTableBundle\Column\Extension\AbstractColumnTypeExtension; +use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class TrimColumnTypeExtension extends AbstractColumnTypeExtension +{ + public function buildValueView(ColumnValueView $view, ColumnInterface $column, array $options): void + { + $value = $view->vars['value']; + + if (!$options['trim'] || !is_string($value)) { + return; + } + + $view->vars['value'] = trim($value); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setDefault('trim', true) + ->setAllowedTypes('country_code', 'bool') + ; + } + + public static function getExtendedTypes(): iterable + { + return [TextColumnType::class]; + } +} +``` + +Now, automatically, the [`TextColumnType`](../../reference/types/column/text.md) type, as well as every other type that uses it as a parent, have a `trim` option available, +and its value is trimmed based on this option. + +If your extension aims to cover every column type in the system, provide the base [`ColumnType`](../../reference/types/column/column.md) in the `getExtendedTypes()` method. diff --git a/docs/src/docs/components/exporters.md b/docs/src/docs/components/exporters.md new file mode 100644 index 00000000..25cb923d --- /dev/null +++ b/docs/src/docs/components/exporters.md @@ -0,0 +1,74 @@ +# Exporters + +[[toc]] + +## Adding exporters + +To add an exporter, use the data table builder's `addExporter()` method: + +```php +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Kreyu\Bundle\DataTableBundle\Bridge\OpenSpout\Exporter\Type\CsvExporterType; +use Kreyu\Bundle\DataTableBundle\Bridge\OpenSpout\Exporter\Type\XlsxExporterType; + +class UserDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + $builder + ->addExporter('csv', CsvExporterType::class) + ->addExporter('xlsx', XlsxExporterType::class) + ; + } +} +``` + +This method accepts _three_ arguments: + +- exporter name; +- exporter type — with a fully qualified class name; +- exporter options — defined by the exporter type, used to configure the exporter; + +For reference, see [available exporter types](../../reference/types/exporter.md). + +## Creating exporter types + +Exporter types are classes that implement [`ExporterTypeInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/ExporterTypeInterface.php). However, it's better to extend from the [`AbstractExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/AbstractExporterType.php): + +```php +use Kreyu\Bundle\DataTableBundle\Exporter\Type\AbstractExporterType; + +class CustomExporterType extends AbstractExporterType +{ +} +``` + +
+ +Recommended namespace for the exporter type classes is `App\DataTable\Exporter\Type\`. + +
+ +## Exporter type inheritance + +To make a type class use another type as a parent, provide its fully-qualified class name in the `getParent()` method: + +```php +use Kreyu\Bundle\DataTableBundle\Exporter\Type\AbstractExporterType; +use Kreyu\Bundle\DataTableBundle\Exporter\Type\CallbackExporterType; + +class CustomExporterType extends AbstractExporterType +{ + public function getParent(): ?string + { + return CallbackExporterType::class; + } +} +``` + +::: tip +If you take a look at the [`AbstractExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/AbstractExporterType.php), +you'll see that `getParent()` method returns fully-qualified name of the [`ExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/ExporterType.php) type class. +This is the type that defines all the basic options, such as `label`, `use_headers`, etc. +::: diff --git a/docs/src/docs/components/filters.md b/docs/src/docs/components/filters.md new file mode 100644 index 00000000..f1cdf9c2 --- /dev/null +++ b/docs/src/docs/components/filters.md @@ -0,0 +1,394 @@ +# Filters + +[[toc]] + +## Adding filters + +To add a filter, use the data table builder's `addFilter()` method: + +```php +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\NumberFilterType; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\DateTimeFilterType; + +class UserDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + $builder + ->addFilter('id', NumberFilterType::class) + ->addFilter('name', TextFilterType::class) + ->addFilter('createdAt', DateTimeFilterType::class) + ; + } +} +``` + +This method accepts _three_ arguments: + +- filter name; +- filter type — with a fully qualified class name; +- filter options — defined by the filter type, used to configure the filter; + +For reference, see [available filter types](../../reference/types/filter.md). + +## Creating filter types + +This bundle and integrations, such as [Doctrine ORM integration bundle](../integrations/doctrine-orm/installation.md), come with plenty of the [filter types](../../reference/types/filter.md). +However, those may not cover complex cases. Luckily, creating custom filter types are easy. + +Filter types are classes that implement [`FilterTypeInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/FilterTypeInterface.php). However, it's better to extend from the [`AbstractFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/AbstractFilterType.php): + +```php +use Kreyu\Bundle\DataTableBundle\Filter\Type\AbstractFilterType; + +class PhoneNumberFilterType extends AbstractFilterType +{ +} +``` + +
+ +Recommended namespace for the filter type classes is `App\DataTable\Filter\Type\`. + +
+ +### Filter type inheritance + +If you take a look at the [`AbstractFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/AbstractFilterType.php), you'll see that `getParent()` method returns fully-qualified name of the `FilterType` type class. +This is the type that defines all the required options, such as `label`, `form_type`, `form_options`, etc. + +::: danger This is not recommended: do _not_ use PHP inheritance! +```php +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType; + +class PhoneNumberFilterType extends TextFilterType +{ +} +``` +::: + +::: tip This is recommended: provide parent using the `getParent()` method +```php +use Kreyu\Bundle\DataTableBundle\Filter\Type\AbstractFilterType; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType; + +class PhoneNumberFilterType extends AbstractFilterType +{ + public function getParent(): ?string + { + return TextFilterType::class; + } +} +``` +::: + +Both methods _will work_, but using PHP inheritance may result in unexpected behavior when using the [filter type extensions](#filter-type-extensions). + +### Form type and options + +To define form type and its options for the filter, use `form_type` and `form_options` options: + +```php +use Kreyu\Bundle\DataTableBundle\Filter\Type\AbstractFilterType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class ColorFilterType extends AbstractFilterType +{ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'form_type' => ChoiceType::class, + 'form_options' => [ + 'choices' => [ + '#F44336' => 'Red', + '#4CAF50' => 'Green', + '#2196F3' => 'Blue', + ], + ], + ]); + } +} +``` + +### Creating filter handler + +Filter type classes is used to define the filter, not the actual logic executed when the filter is used. +This logic should be delegated to a filter handler instead. Filter handlers are classes that implement [`FilterHandlerInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/FilterHandlerInterface.php): + +```php +use Kreyu\Bundle\DataTableBundle\Filter\FilterHandlerInterface; +use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface; +use Kreyu\Bundle\DataTableBundle\Filter\FilterData; +use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface; + +class CustomFilterHandler implements FilterHandlerInterface +{ + public function handle(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter): void + { + // ... + } +} +``` +
+ +For example, take a look at the [`DoctrineOrmFilterHandler`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/DoctrineOrmFilterHandler.php), +which is used by all Doctrine ORM integration filter types. + +
+ +The filter handler can be applied to a custom filter type by using the filter builder's `setHandler()` method: + +```php +use Kreyu\Bundle\DataTableBundle\Filter\Type\AbstractFilterType; +use Kreyu\Bundle\DataTableBundle\Filter\FilterBuilderInterface; + +class CustomFilterType extends AbstractFilterType +{ + public function buildFilter(FilterBuilderInterface $builder, array $options): void + { + $builder->setHandler(new CustomFilterHandler()); + } +} +``` + +If separate class seems like an overkill, you can implement the handler interface on the type class instead: + +```php +use Kreyu\Bundle\DataTableBundle\Filter\Type\AbstractFilterType; +use Kreyu\Bundle\DataTableBundle\Filter\FilterBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Filter\FilterHandlerInterface; +use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface; +use Kreyu\Bundle\DataTableBundle\Filter\FilterData; +use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface; + +class CustomFilterType extends AbstractFilterType implements FilterHandlerInterface +{ + public function buildFilter(FilterBuilderInterface $builder, array $options): void + { + $builder->setHandler($this); + } + + public function handle(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter): void + { + // ... + } +} +``` + +## Filter type extensions + +Filter type extensions allows modifying configuration of the existing filter types, even the built-in ones. +Let's assume, that we want to [change default operator](#changing-default-operator) of [`TextFilterType`](#) +to `Operator::Equals`, so it is not necessary to pass `default_operator` option for each filter using this type. + +Filter type extensions are classes that implement [`FilterTypeExtensionInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Extension/FilterTypeExtensionInterface.php). +However, it's better to extend from the [`AbstractFilterTypeExtension`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Extension/AbstractColumnTypeExtension.php): + +```php +use Kreyu\Bundle\DataTableBundle\Filter\Extension\AbstractFilterTypeExtension; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType; +use Kreyu\Bundle\DataTableBundle\Filter\Operator; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class DefaultOperatorTextFilterTypeExtension extends AbstractFilterTypeExtension +{ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefault('default_operator', Operator::Equals); + } + + public static function getExtendedTypes(): iterable + { + return [TextFilterType::class]; + } +} +``` + +If your extension aims to cover every filter type in the system, provide the base [`FilterType`](#) in the `getExtendedTypes()` method. + +## Formatting active filter value + +When the filter is active, its value is rendered to the user as a "pill", which removes the filter upon clicking it. +By default, the filter value requires to be stringable. However, there are some cases, where value cannot be stringable. + +Let's assume, that the application contains a `Product` entity, which contains a `Category`, which is **not** stringable: + +```php +readonly class Product +{ + public function __construct( + public Category $category, + ) +} + +readonly class Category +{ + public function __construct( + public string $name, + ) +} +``` + +In the product data table, we want to filter products by their category. +Using [EntityFilterType](#) will allow selecting a category from a list of existing categories. +Unfortunately, when the filter is applied, a `Cannot convert value of type Category to string` exception will occur. + +In that case, you can use the `active_filter_formatter` option, to determine what should be rendered based on the filter data: + +```php +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\EntityFilterType; +use Kreyu\Bundle\DataTableBundle\Filter\Operator; + +class ProductDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + $builder + ->addFilter('category', EntityFilterType::class, [ + 'form_options' => [ + 'class' => Category::class, + 'choice_label' => 'name', + ], + 'active_filter_formatter' => function (FilterData $data) { + $value = $data->getValue(); + + if ($value instanceof Category) { + return $value->getName(); + } + + return $value; + }, + ]) + ; + } +} +``` + +::: tip This is only a simple example of using the `active_filter_formatter` option. +The [`EntityFilterType`](#) has a `choice_label` option, which can be used to provide property path to the value to render: + +```php +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\EntityFilterType; + +class ProductDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + $builder + ->addFilter('category', EntityFilterType::class, [ + 'form_options' => [ + 'class' => Category::class, + 'choice_label' => 'name', + ], + 'choice_label' => 'name', // same as the form choice_label option + ]) + ; + } +} +``` +::: + +## Changing default operator + +Let's assume, that the application contains a `Book` entity with ISBN: + +```php +readonly class Book +{ + public function __construct( + public string $isbn, + ) +} +``` + +If we use a [TextFilterType](#) on the `isbn` column, the filter will perform partial matching (`LIKE %value%`), +because the filter type has `default_operator` option set to `Operator::Contains`. +In this case, we want to perform exact matching, therefore, we have to change this option value to `Operator::Equals`: + +```php +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType; +use Kreyu\Bundle\DataTableBundle\Filter\Operator; + +class ProductDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + $builder + ->addFilter('isbn', TextFilterType::class, [ + 'default_operator' => Operator::Equals, + ]) + ; + } +} +``` + +
+ +Each filter supports different set of operators. + +
+ +
+ +To change default operator filter type without having to explicitly provide the `default_operator`, +consider creating a [filter type extension](#filter-type-extensions). + +
+ +## Displaying operator selector + +The operator can be selected by the user, when operator selector is visible. +By default, operator selector is **not** visible. To change that, use `operator_visible` option: + +```php +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType; +use Kreyu\Bundle\DataTableBundle\Filter\Operator; + +class ProductDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + $builder + ->addFilter('isbn', TextFilterType::class, [ + 'operator_visible' => true, + ]) + ; + } +} +``` + +## Operator form type and options + +You can customize form type and options of the operator form field, using `operator_form_type` and `operator_form_options`: + +```php +use Kreyu\Bundle\DataTableBundle\Filter\Type\AbstractFilterType; +use Kreyu\Bundle\DataTableBundle\Filter\Form\Type\OperatorType; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class ProductFilterType extends AbstractFilterType +{ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + // Note: this is default operator type + 'operator_form_type' => OperatorType::class, + 'operator_form_options' => [ + 'required' => true, + ], + ]); + } +} +``` diff --git a/docs/src/docs/contributing.md b/docs/src/docs/contributing.md new file mode 100644 index 00000000..d1eab678 --- /dev/null +++ b/docs/src/docs/contributing.md @@ -0,0 +1,13 @@ +# Contributing + +## Documentation + +The documentation is powered by the [VitePress](https://vitepress.dev/). + +To locally preview the documentation, first, install the [VitePress](https://vitepress.dev/) locally. +The installation instructions are available in the ["Getting Started" documentation section](https://vitepress.dev/guide/getting-started). +Then, to build the documentation locally (and rebuild when change is detected), run the following command: + +```shell +npm install && npm run docs:dev +``` diff --git a/docs/features/symfony-ux-turbo.md b/docs/src/docs/features/asynchronicity.md old mode 100755 new mode 100644 similarity index 57% rename from docs/features/symfony-ux-turbo.md rename to docs/src/docs/features/asynchronicity.md index e37af714..cd417980 --- a/docs/features/symfony-ux-turbo.md +++ b/docs/src/docs/features/asynchronicity.md @@ -1,9 +1,4 @@ ---- -label: Symfony UX Turbo -order: k ---- - -# Integration with Symfony UX Turbo +# Asynchronicity [Symfony UX Turbo](https://symfony.com/bundles/ux-turbo/current/index.html) is a Symfony bundle integrating the [Hotwire Turbo](https://turbo.hotwired.dev/) library in Symfony applications. It allows having the same user experience as with [Single Page Apps](https://en.wikipedia.org/wiki/Single-page_application) but without having to write a single line of JavaScript! @@ -12,26 +7,27 @@ This bundle provides integration that works out-of-the-box. ## The magic part -To begin with, make sure your application uses the [Symfony UX Turbo](https://symfony.com/bundles/ux-turbo/current/index.html#usage). -The next step is... voilà! ✨ You don't have to configure anything extra, your data tables automatically work asynchronously! -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), +Make sure your application uses the [Symfony UX Turbo](https://symfony.com/bundles/ux-turbo/current/index.html). +You don't have to configure anything extra, your data tables automatically work asynchronously! +The magic comes from the [base template](https://github.com/Kreyu/data-table-bundle/blob/main/src/Resources/views/themes/base.html.twig), which wraps the whole table in the `` tag: -{% raw %} -```twig # @KreyuDataTable/themes/base.html.twig +```twig +{# @KreyuDataTable/themes/base.html.twig #} {% block kreyu_data_table %} {# ... #} {% endblock %} ``` -{% endraw %} This ensures every data table is wrapped in its own frame, making them work asynchronously. -!!! Note -This integration also works on other built-in templates because they all extend the base one. -If you're making a data table theme from scratch, make sure the table is wrapped in the Turbo frame, as shown above. -!!! +
+ +This integration also works on other built-in templates, because they all extend the base one. +If you're making a data table theme from scratch, make sure the table is wrapped in the Turbo frame, as shown above. + +
For more information, see [official documentation about the Turbo frames](https://symfony.com/bundles/ux-turbo/current/index.html#decomposing-complex-pages-with-turbo-frames). diff --git a/docs/features/exporting.md b/docs/src/docs/features/exporting.md old mode 100755 new mode 100644 similarity index 70% rename from docs/features/exporting.md rename to docs/src/docs/features/exporting.md index c5e72fab..c69378e6 --- a/docs/features/exporting.md +++ b/docs/src/docs/features/exporting.md @@ -1,27 +1,27 @@ ---- -order: e ---- - # Exporting -The data tables can be _exported_, with use of the [exporters](../reference/exporters/types.md). +The data tables can be _exported_, with use of the [exporters](#). + +::: details Screenshots +![Export modal with the Tabler theme](/export_modal.png) +::: -![Export modal with the Tabler theme](./../static/export_modal.png) +[[toc]] ## Toggling the feature By default, the exporting feature is **enabled** for every data table. This can be configured with the `exporting_enabled` option: -+++ Globally (YAML) -```yaml # config/packages/kreyu_data_table.yaml +::: code-group +```yaml [Globally (YAML)] kreyu_data_table: defaults: exporting: enabled: true ``` -+++ Globally (PHP) -```php # config/packages/kreyu_data_table.php + +```php [Globally (PHP)] use Symfony\Config\KreyuDataTableConfig; return static function (KreyuDataTableConfig $config) { @@ -29,8 +29,8 @@ return static function (KreyuDataTableConfig $config) { $defaults->exporting()->enabled(true); }; ``` -+++ For data table type -```php # src/DataTable/Type/ProductDataTable.php + +```php [For data table type] use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -44,8 +44,8 @@ class ProductDataTableType extends AbstractDataTableType } } ``` -+++ For specific data table -```php # src/Controller/ProductController.php + +```php [For specific data table] use App\DataTable\Type\ProductDataTableType; use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -66,17 +66,17 @@ class ProductController extends AbstractController } } ``` -+++ +::: -!!! Enabling the feature does not mean that any column will be exportable by itself. +::: tip Enabling the feature does not mean that any column will be exportable by itself. By default, columns **are not** exportable. -!!! +::: -### Making the columns exportable +## Making the columns exportable To make any column exportable, use its `export` option: -```php # src/DataTable/Type/ProductDataTableType.php +```php use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; use Kreyu\Bundle\DataTableBundle\Column\Type\NumberColumnType; use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; @@ -97,7 +97,7 @@ class ProductDataTableType extends AbstractDataTableType The column can be configured separately for the export by providing the array in the `export` option. For example, to change the label of the column in the export: -```php # src/DataTable/Type/ProductDataTableType.php +```php use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType; use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; @@ -117,42 +117,12 @@ class ProductDataTableType extends AbstractDataTableType } ``` -## Adding the exporters - -To add exporter, use the builder's `addExporter()` method on the data table builder: - -```php # src/DataTable/Type/ProductDataTableType.php -use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; -use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; -use Kreyu\Bundle\DataTableOpenSpoutBundle\Exporter\Type\CsvExporterType; -use Kreyu\Bundle\DataTableOpenSpoutBundle\Exporter\Type\XlsxExporterType; - -class ProductDataTableType extends AbstractDataTableType -{ - public function buildDataTable(DataTableBuilderInterface $builder, array $options): void - { - $builder - ->addExporter('csv', CsvExporterType::class) - ->addExporter('xlsx', XlsxExporterType::class) - ; - } -} -``` - -The builder's `addExporter()` method accepts _three_ arguments: - -- exporter name; -- exporter type — with a fully qualified class name; -- exporter options — defined by the exporter type, used to configure the exporter; - -For reference, see [built-in exporter types](../reference/exporters/types.md). +## Default export configuration -## Configuring default export data - -The default export data, such as filename, exporter, strategy and a flag to include personalization, +The default export data, such as filename, exporter, strategy and a flag whether the personalization should be included, can be configured using the data table builder's `setDefaultExportData()` method: -```php # src/DataTable/Type/ProductDataTableType.php +```php use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; use Kreyu\Bundle\DataTableBundle\Exporter\ExportData; use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; @@ -177,7 +147,7 @@ class ProductDataTableType extends AbstractDataTableType 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 +```php use App\DataTable\Type\ProductDataTableType; use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -201,9 +171,9 @@ class ProductController extends AbstractController ## Exporting without user input -To export the data table manually, without user input, use the `export()` method directly: +To export the data table manually, without user input, use the `export()` method directly: -```php #13-14 src/Controller/ProductController.php +```php use App\DataTable\Type\ProductDataTableType; use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -228,10 +198,9 @@ class ProductController extends AbstractController } ``` -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: +The export data (configuration, e.g. a filename) can be included by passing it directly to the `export()` method: -```php #13-14,16 src/Controller/ProductController.php +```php use App\DataTable\Type\ProductDataTableType; use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -255,12 +224,12 @@ class ProductController extends AbstractController } ``` -## Exporting optimization +## Optimization with Doctrine ORM -The exporting process including all pages of the large datasets can take a very long time. +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 +```php use Doctrine\ORM\AbstractQuery; use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; use Kreyu\Bundle\DataTableBundle\Event\DataTableEvent; @@ -281,7 +250,7 @@ class ProductDataTableType extends AbstractDataTableType 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 # src/DataTable/Type/ProductDataTableType.php +```php use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; use Kreyu\Bundle\DataTableBundle\Column\Type\NumberColumnType; use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; @@ -304,13 +273,16 @@ class ProductDataTableType extends AbstractDataTableType ## 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: +The following events are dispatched when `export()` method of the [`DataTableInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/DataTableInterface.php) is called: + +::: info PRE_EXPORT +Dispatched before the exporter is called. +Can be used to modify the exporting data (configuration), e.g. to force an export strategy or change the filename. -[: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. +**See**: [`DataTableEvents::PRE_EXPORT`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php) +::: -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): +The dispatched events are instance of the [`DataTablePersonalizationEvent`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTablePersonalizationEvent.php): ```php use Kreyu\Bundle\DataTableBundle\Event\DataTableExportEvent; @@ -322,7 +294,7 @@ class DataTableExportListener $dataTable = $event->getDataTable(); $exportData = $event->getExportData(); - // for example, modify the export data, then save it in the event + // for example, modify the export data (configuration), then save it in the event $event->setExportData($exportData); } } diff --git a/docs/src/docs/features/extensibility.md b/docs/src/docs/features/extensibility.md new file mode 100644 index 00000000..4bd655d5 --- /dev/null +++ b/docs/src/docs/features/extensibility.md @@ -0,0 +1,270 @@ +# Extensibility + +There are multiple concepts that can be modified for a specific case. + +[[toc]] + +## Request handlers + +The data tables by default have no clue about the requests. +To solve this problem, a request can be handled by the data table using the `handleRequest()` method. +This means an underlying request handler will be called, extracting the required data from the request, +and calling methods such as `sort()` or `paginate()` on the data table. + +### Built-in request handlers + +This bundle comes with [HttpFoundationRequestHandler](https://github.com/Kreyu/data-table-bundle/blob/main/src/Request/HttpFoundationRequestHandler.php), +which supports the [request object](https://github.com/symfony/http-foundation/blob/6.4/Request.php) common for the Symfony applications: + +```php +use App\DataTable\Type\ProductDataTableType; +use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; + +class ProductController extends AbstractController +{ + use DataTableFactoryAwareTrait; + + public function index(Request $request) + { + $dataTable = $this->createDataTable(ProductDataTableType::class); + $dataTable->handleRequest($request); + } +} +``` + +### Creating request handlers + +To create a request handler, create a class that implements [RequestHandlerInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Request/RequestHandlerInterface.php): + +```php +use Kreyu\Bundle\DataTableBundle\Request\RequestHandlerInterface; +use Kreyu\Bundle\DataTableBundle\Sorting\SortingData; +use Kreyu\Bundle\DataTableBundle\Sorting\SortingField; + +class CustomRequestHandler implements RequestHandlerInterface +{ + public function handle(DataTableInterface $dataTable, mixed $request = null): void + { + // Call desired methods with arguments based on the data from $request + $dataTable->paginate(...); + $dataTable->sort(...); + $dataTable->personalize(...); + $dataTable->filter(...); + $dataTable->export(...); + } +} +``` + +
+ +The recommended namespace for the request handlers is `App\DataTable\Request`. + +
+ +You can apply this request handler globally using the configuration file, or use `request_handler` option: + +::: code-group +```yaml [Globally (YAML)] +kreyu_data_table: + defaults: + # this should be a service id - which is class by default + request_handler: 'App\DataTable\Request\CustomRequestHandler' +``` + +```php [Globally (PHP)] +use Symfony\Config\KreyuDataTableConfig; + +return static function (KreyuDataTableConfig $config) { + $defaults = $config->defaults(); + // this should be a service id - which is class by default + $defaults->requestHandler('App\DataTable\Request\CustomRequestHandler'); +}; +``` + +```php [For data table type] +use App\DataTable\Request\CustomRequestHandler; +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class ProductDataTableType extends AbstractDataTableType +{ + public function __construct( + private CustomRequestHandler $requestHandler, + ) { + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'request_handler' => $this->requestHandler, + ]); + } +} +``` + +```php [For specific data table] +use App\DataTable\Request\CustomRequestHandler; +use App\DataTable\Type\ProductDataTableType; +use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + +class ProductController extends AbstractController +{ + use DataTableFactoryAwareTrait; + + public function __construct( + private CustomRequestHandler $requestHandler, + ) { + } + + public function index() + { + $dataTable = $this->createDataTable( + type: ProductDataTableType::class, + query: $query, + options: [ + 'request_handler' => $this->requestHandler, + ], + ); + } +} +``` +::: + +## Proxy queries + +This bundle is data source agnostic, meaning it is not tied to any specific ORM, such as Doctrine ORM. +This is accomplished thanks to **proxy queries**, which work as an adapter for the specific data source. + +### Creating custom proxy query + +To create a custom proxy query, create a class that implements [ProxyQueryInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Query/ProxyQueryInterface.php): + +```php +use Kreyu\Bundle\DataTableBundle\Pagination\PaginationData; +use Kreyu\Bundle\DataTableBundle\Sorting\SortingData; +use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface; +use Kreyu\Bundle\DataTableBundle\Query\ResultSetInterface; + +class ArrayProxyQuery implements ProxyQueryInterface +{ + public function __construct( + private array $data, + ) { + } + + public function sort(SortingData $sortingData): void + { + } + + public function paginate(PaginationData $paginationData): void + { + } + + public function getResult(): ResultSetInterface + { + } +} +``` + +
+ +The recommended namespace for the proxy queries is `App\DataTable\Query`. + +
+ +Now you can use the custom proxy query when creating the data tables: + +```php +use App\DataTable\Type\ProductDataTableType; +use App\DataTable\Query\ArrayProxyQuery; +use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + +class ProductController extends AbstractController +{ + use DataTableFactoryAwareTrait; + + public function index() + { + // Note: the products are an instance of ArrayProxyQuery + $products = new ArrayProxyQuery([ + new Product(name: 'Product #1'), + new Product(name: 'Product #2'), + new Product(name: 'Product #3'), + ]); + + $dataTable = $this->createDataTable(ProductDataTableType::class, $products); + } +} +``` + +### Creating proxy query factory + +Each proxy query should have a factory, so the bundle can handle passing the raw data like so: + +```php +use App\DataTable\Type\ProductDataTableType; +use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + +class ProductController extends AbstractController +{ + use DataTableFactoryAwareTrait; + + public function index() + { + // Note: products are just a simple array, ArrayProxyQuery is not required + $products = [ + new Product(name: 'Product #1'), + new Product(name: 'Product #2'), + new Product(name: 'Product #3'), + ]; + + $dataTable = $this->createDataTable(ProductDataTableType::class, $products); + } +} +``` + +Without dedicated proxy query factory to handle array data, the bundle will throw an exception: + +> Unable to create ProxyQuery for given data + +In the background, the [ChainProxyQueryFactory](https://github.com/Kreyu/data-table-bundle/blob/main/src/Query/ChainProxyQueryFactory.php) +iterates through registered proxy query factories, and returns the first successfully created proxy query. +The error occurs because there is no factory to create the custom type. + +To create a proxy query factory, create a class that implements the [ProxyQueryFactoryInterface](https://github.com/Kreyu/data-table-bundle/blob/main/src/Query/ProxyQueryFactoryInterface.php): + +```php +use App\DataTable\Query\ArrayProxyQuery; +use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryFactoryInterface; +use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface; + +class ArrayProxyQueryFactory implements ProxyQueryFactoryInterface +{ + public function create(mixed $data): ProxyQueryInterface + { + if (!is_array($data)) { + throw new UnexpectedTypeException($data, ArrayProxyQuery::class); + } + + return new ArrayProxyQuery($data); + } +} +``` + +
+ +The recommended namespace for the proxy query factories is `App\DataTable\Query`. + +
+ +If the custom proxy query does not support a specific data class, the factory **have** to throw an [UnexpectedTypeException](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exception/UnexpectedTypeException.php), +so the chain proxy query factory will know to skip that factory and check other ones. + +Proxy query factories must be registered as services and tagged with the `kreyu_data_table.proxy_query.factory` tag. +If you're using the [default services.yaml configuration](https://symfony.com/doc/current/service_container.html#service-container-services-load-example), +this is already done for you, thanks to [autoconfiguration](https://symfony.com/doc/current/service_container.html#services-autoconfigure). diff --git a/docs/src/docs/features/filtering.md b/docs/src/docs/features/filtering.md new file mode 100644 index 00000000..16a865dd --- /dev/null +++ b/docs/src/docs/features/filtering.md @@ -0,0 +1,225 @@ +# Filtering + +The data tables can be _filtered_, with use of the [filters](#). + +[[toc]] + +## Toggling the feature + +By default, the filtration feature is **enabled** for every data table. +This can be configured with the `filtration_enabled` option: + +::: code-group +```yaml [Globally (YAML)] +kreyu_data_table: + defaults: + filtration: + enabled: true +``` + +```php [Globally (PHP)] +use Symfony\Config\KreyuDataTableConfig; + +return static function (KreyuDataTableConfig $config) { + $defaults = $config->defaults(); + $defaults->filtration()->enabled(true); +}; +``` + +```php [For data table type] +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class ProductDataTableType extends AbstractDataTableType +{ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'filtration_enabled' => true, + ]); + } +} +``` + +```php [For specific data table] +use App\DataTable\Type\ProductDataTableType; +use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + +class ProductController extends AbstractController +{ + use DataTableFactoryAwareTrait; + + public function index() + { + $dataTable = $this->createDataTable( + type: ProductDataTableType::class, + query: $query, + options: [ + 'filtration_enabled' => true, + ], + ); + } +} +``` +::: + +## Saving applied filters + +By default, the filtration feature [persistence](persistence.md) is **disabled** for every data table. + +You can configure the [persistence](persistence.md) globally using the package configuration file, or its related options: + +::: code-group +```yaml [Globally (YAML)] +kreyu_data_table: + defaults: + filtration: + persistence_enabled: true + # if persistence is enabled and symfony/cache is installed, null otherwise + persistence_adapter: kreyu_data_table.filtration.persistence.adapter.cache + # if persistence is enabled and symfony/security-bundle is installed, null otherwise + persistence_subject_provider: kreyu_data_table.persistence.subject_provider.token_storage +``` + +```php [Globally (PHP)] +use Symfony\Config\KreyuDataTableConfig; + +return static function (KreyuDataTableConfig $config) { + $defaults = $config->defaults(); + $defaults->filtration() + ->persistenceEnabled(true) + // if persistence is enabled and symfony/cache is installed, null otherwise + ->persistenceAdapter('kreyu_data_table.filtration.persistence.adapter.cache') + // if persistence is enabled and symfony/security-bundle is installed, null otherwise + ->persistenceSubjectProvider('kreyu_data_table.persistence.subject_provider.token_storage') + ; +}; +``` + +```php [For data table type] +use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface; +use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface; +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class ProductDataTableType extends AbstractDataTableType +{ + public function __construct( + #[Autowire(service: 'kreyu_data_table.filtration.persistence.adapter.cache')] + private PersistenceAdapterInterface $persistenceAdapter, + #[Autowire(service: 'kreyu_data_table.persistence.subject_provider.token_storage')] + private PersistenceSubjectProviderInterface $persistenceSubjectProvider, + ) { + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'filtration_persistence_enabled' => true, + 'filtration_persistence_adapter' => $this->persistenceAdapter, + 'filtration_persistence_subject_provider' => $this->persistenceSubjectProvider, + ]); + } +} +``` + +```php [For specific data table] +use App\DataTable\Type\ProductDataTableType; +use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; +use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface; +use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + +class ProductController extends AbstractController +{ + use DataTableFactoryAwareTrait; + + public function __construct( + #[Autowire(service: 'kreyu_data_table.filtration.persistence.adapter.cache')] + private PersistenceAdapterInterface $persistenceAdapter, + #[Autowire(service: 'kreyu_data_table.persistence.subject_provider.token_storage')] + private PersistenceSubjectProviderInterface $persistenceSubjectProvider, + ) { + } + + public function index() + { + $dataTable = $this->createDataTable( + type: ProductDataTableType::class, + query: $query, + options: [ + 'filtration_persistence_enabled' => true, + 'filtration_persistence_adapter' => $this->persistenceAdapter, + 'filtration_persistence_subject_provider' => $this->persistenceSubjectProvider, + ], + ); + } +} +``` +::: + +## Default filtration + +The default filtration data can be overridden using the data table builder's `setDefaultFiltrationData()` method: + +```php +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Kreyu\Bundle\DataTableBundle\Filter\FiltrationData; +use Kreyu\Bundle\DataTableBundle\Filter\FilterData; +use Kreyu\Bundle\DataTableBundle\Filter\Operator; + +class ProductDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + $builder->setDefaultFiltrationData(new FiltrationData([ + 'name' => new FilterData(value: 'John', operator: Operator::Contains), + ])); + + // or by creating the filtration data from an array: + $builder->setDefaultFiltrationData(FiltrationData::fromArray([ + 'name' => ['value' => 'John', 'operator' => 'contains'], + ])); + } +} +``` + +## Events + +The following events are dispatched when `filter()` method of the [`DataTableInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/DataTableInterface.php) is called: + +::: info PRE_FILTER +Dispatched before the filtration data is applied to the query. +Can be used to modify the filtration data, e.g. to force application of some filters. + +**See**: [`DataTableEvents::PRE_FILTER`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php) +::: + +::: info POST_FILTER +Dispatched after the filtration data is applied to the query and saved if the filtration persistence is enabled; +Can be used to execute additional logic after the filters are applied. + +**See**: [`DataTableEvents::POST_FILTER`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php) +::: + +The dispatched events are instance of the [`DataTableFiltrationEvent`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableFiltrationEvent.php): + +```php +use Kreyu\Bundle\DataTableBundle\Event\DataTableFiltrationEvent; + +class DataTableFiltrationListener +{ + public function __invoke(DataTableFiltrationEvent $event): void + { + $dataTable = $event->getDataTable(); + $filtrationData = $event->getFiltrationData(); + + // for example, modify the filtration data, then save it in the event + $event->setFiltrationData($filtrationData); + } +} +``` \ No newline at end of file diff --git a/docs/features/pagination.md b/docs/src/docs/features/pagination.md old mode 100755 new mode 100644 similarity index 74% rename from docs/features/pagination.md rename to docs/src/docs/features/pagination.md index cbeeaf9b..07a6f1cf --- a/docs/features/pagination.md +++ b/docs/src/docs/features/pagination.md @@ -1,25 +1,23 @@ ---- -order: d ---- - # Pagination The data tables can be _paginated_, which is crucial when working with large data sources. +[[toc]] + ## Toggling the feature By default, the pagination feature is **enabled** for every data table. This can be configured thanks to the `pagination_enabled` option: -+++ Globally (YAML) -```yaml # config/packages/kreyu_data_table.yaml +::: code-group +```yaml [Globally (YAML)] kreyu_data_table: defaults: pagination: enabled: true ``` -+++ Globally (PHP) -```php # config/packages/kreyu_data_table.php + +```php [Globally (PHP)] use Symfony\Config\KreyuDataTableConfig; return static function (KreyuDataTableConfig $config) { @@ -27,8 +25,8 @@ return static function (KreyuDataTableConfig $config) { $defaults->pagination()->enabled(true); }; ``` -+++ For data table type -```php # src/DataTable/Type/ProductDataTable.php + +```php [For data table type] use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -42,8 +40,8 @@ class ProductDataTableType extends AbstractDataTableType } } ``` -+++ For specific data table -```php # src/Controller/ProductController.php + +```php [For specific data table] use App\DataTable\Type\ProductDataTableType; use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -64,22 +62,22 @@ class ProductController extends AbstractController } } ``` -+++ +::: -!!! If you don't see the pagination controls, make sure your data table has enough records! +::: tip If you don't see the pagination controls, make sure your data table has enough records! By default, every page contains 25 records. Built-in themes display pagination controls only when the data table contains more than one page. -Also, remember that you can [change the default pagination data](#configuring-default-pagination), reducing the per-page limit. -!!! +Also, remember that you can [change the default pagination data](#default-pagination), reducing the per-page limit. +::: -## Configuring the feature persistence +## Saving applied pagination By default, the pagination feature [persistence](persistence.md) is **disabled** for every data table. You can configure the [persistence](persistence.md) globally using the package configuration file, or its related options: -+++ Globally (YAML) -```yaml # config/packages/kreyu_data_table.yaml +::: code-group +```yaml [Globally (YAML)] kreyu_data_table: defaults: pagination: @@ -89,8 +87,8 @@ kreyu_data_table: # if persistence is enabled and symfony/security-bundle is installed, null otherwise persistence_subject_provider: kreyu_data_table.persistence.subject_provider.token_storage ``` -+++ Globally (PHP) -```php # config/packages/kreyu_data_table.php + +```php [Globally (PHP)] use Symfony\Config\KreyuDataTableConfig; return static function (KreyuDataTableConfig $config) { @@ -104,8 +102,8 @@ return static function (KreyuDataTableConfig $config) { ; }; ``` -+++ For data table type -```php # src/DataTable/Type/ProductDataTable.php + +```php [For data table type] use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface; use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface; use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; @@ -124,13 +122,13 @@ class ProductDataTableType extends AbstractDataTableType $resolver->setDefaults([ 'pagination_persistence_enabled' => true, 'pagination_persistence_adapter' => $this->persistenceAdapter, - 'pagination_persistence_subject' => $this->persistenceSubjectProvider->provide(), + 'pagination_persistence_subject_provider' => $this->persistenceSubjectProvider, ]); } } ``` -+++ For specific data table -```php # src/Controller/ProductController.php + +```php [For specific data table] use App\DataTable\Type\ProductDataTableType; use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface; @@ -155,15 +153,15 @@ class ProductController extends AbstractController options: [ 'pagination_persistence_enabled' => true, 'pagination_persistence_adapter' => $this->persistenceAdapter, - 'pagination_persistence_subject' => $this->persistenceSubjectProvider->provide(), + 'pagination_persistence_subject_provider' => $this->persistenceSubjectProvider, ], ); } } ``` -+++ +::: -## Configuring default pagination +## Default pagination The default pagination data can be overridden using the data table builder's `setDefaultPaginationData()` method: @@ -192,17 +190,23 @@ 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: +The following events are dispatched when `paginate()` method of the [`DataTableInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/DataTableInterface.php) is called: + +::: info PRE_PAGINATE +Dispatched before the pagination data is applied to the query. +Can be used to modify the pagination data, e.g. to force specific page or a per-page limit. -[: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. +**See**: [`DataTableEvents::PRE_PAGINATE`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php) +::: -[: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. +::: info POST_PAGINATE +Dispatched after the pagination data is applied to the query and saved if the pagination persistence is enabled. +Can be used to execute additional logic after the pagination is applied. -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): +**See**: [`DataTableEvents::POST_PAGINATE`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php) +::: + +The dispatched events are instance of the [`DataTablePaginationEvent`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTablePaginationEvent.php): ```php use Kreyu\Bundle\DataTableBundle\Event\DataTablePaginationEvent; @@ -218,4 +222,4 @@ class DataTablePaginationListener $event->setPaginationData($paginationData); } } -``` \ No newline at end of file +``` diff --git a/docs/features/persistence.md b/docs/src/docs/features/persistence.md old mode 100755 new mode 100644 similarity index 61% rename from docs/features/persistence.md rename to docs/src/docs/features/persistence.md index c8ceba1c..73645597 --- a/docs/features/persistence.md +++ b/docs/src/docs/features/persistence.md @@ -1,33 +1,28 @@ ---- -order: g ---- - # Persistence -This bundle provides persistence feature, ready to use with data table sorting, pagination, filtration and personalization. +This bundle provides a persistence feature, which is used to save data between requests. +For example, it can be used to persist applied filters or pagination, per user. + +[[toc]] ## Toggling the feature Persistence can be toggled per feature with its own configuration: -- [Features > Pagination > Configuring the feature persistence](pagination.md#configuring-the-feature-persistence) -- [Features > Sorting > Configuring the feature persistence](sorting.md#configuring-the-feature-persistence) -- [Features > Filtering > Configuring the feature persistence](filtering.md#configuring-the-feature-persistence) -- [Features > Personalization > Configuring the feature persistence](personalization.md#configuring-the-feature-persistence) +- [Saving applied pagination](pagination.md#saving-applied-pagination) +- [Saving applied sorting](sorting.md#configuring-the-feature-persistence) +- [Saving applied filters](filtering.md#configuring-the-feature-persistence) +- [Saving applied personalization](personalization.md#saving-applied-personalization) ## Persistence adapters Adapters are classes that allow writing (to) and reading (from) the persistent data source. -### Built-in adapters - -By default, there's only one adapter integrating the [Symfony Cache](https://symfony.com/doc/current/components/cache.html). -It accepts two arguments in the constructor: +### Built-in cache adapter -- a cache implementing Symfony's [:icon-mark-github: CacheInterface](https://github.com/symfony/contracts/blob/main/Cache/CacheInterface.php) -- prefix string used to differentiate different data sets, e.g. filtration persistence uses `filtration` prefix +The bundle has a built-in cache adapter, which uses the [Symfony Cache component](https://symfony.com/doc/current/components/cache.html). -In service container, it is registered as an [abstract service](https://symfony.com/doc/current/service_container/parent_services.html): +It is registered as an [abstract service](https://symfony.com/doc/current/service_container/parent_services.html) in the service container: ```bash $ bin/console debug:container kreyu_data_table.persistence.adapter.cache @@ -42,11 +37,17 @@ $ bin/console debug:container kreyu_data_table.filtration.persistence.adapter.ca $ bin/console debug:container kreyu_data_table.personalization.persistence.adapter.cache ``` +The bundle adds a `kreyu_data_table.persistence.cache.default` cache pool, which uses the `cache.adapter.filesystem` adapter, with `tags` enabled. + +::: tip It is recommended to use tag-aware cache adapter! +The built-in [cache persistence clearer](#persistence-clearers) requires tag-aware cache to clear persistence data. +::: + ### Creating custom adapters -To create a custom adapter, create a class that implements `PersistenceAdapterInterface`: +To create a custom adapter, create a class that implements `PersistenceAdapterInterface`. -```php # src/DataTable/Persistence/DatabasePersistenceAdapter.php +```php use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface; class DatabasePersistenceAdapter implements PersistenceAdapterInterface @@ -69,14 +70,19 @@ class DatabasePersistenceAdapter implements PersistenceAdapterInterface } ``` -!!! -The recommended namespace for the persistence adapters is `App\DataTable\Persistence`. -!!! +
-...and register it in the container as an abstract service: +Recommended namespace for the column type classes is `App\DataTable\Persistence\`. -+++ YAML -```yaml # config/services.yaml +
+ +The recommended way of creating those classes is accepting a `prefix` argument in the constructor. +This prefix will be different for each feature, for example, personalization persistence will use `personalization` prefix. + +Now, register it in the container as an abstract service: + +::: code-group +```yaml [YAML] services: app.data_table.persistence.database: class: App\DataTable\Persistence\DatabasePersistenceAdapter @@ -84,26 +90,25 @@ services: arguments: - '@doctrine.orm.entity_manager' ``` -+++ PHP -```php # config/services.php + +```php [PHP] use App\DataTable\Persistence\DatabasePersistenceAdapter; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $configurator) { - $services = $configurator->services(); - $services + $configurator->services() ->set('app.data_table.persistence.database', DatabasePersistenceAdapter::class) - ->args([service('doctrine.orm.entity_manager')]) - ->abstract() + ->args([service('doctrine.orm.entity_manager')]) + ->abstract() ; ``` -+++ +::: Now, create as many adapters as you need, based on the abstract definition. -For example, let's create an adapter for sorting feature, prefixed with "sorting". +For example, let's create an adapter for personalization feature, using the `personalization` prefix: -+++ YAML -```yaml # config/services.yaml +::: code-group +```yaml [YAML] services: app.data_table.personalization.persistence.database: parent: app.data_table.persistence.database @@ -112,43 +117,42 @@ services: tags: - { name: kreyu_data_table.proxy_query.factory } ``` -+++ PHP -```php # config/services.php + +```php [PHP] use App\DataTable\Persistence\DatabasePersistenceAdapter; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $configurator) { - $services = $configurator->services(); - $services + $configurator->services() ->set('app.data_table.personalization.persistence.database') - ->parent('app.data_table.persistence.database') - ->arg('$prefix', 'personalization') + ->parent('app.data_table.persistence.database') + ->arg('$prefix', 'personalization') ; ``` -+++ +::: The data tables can now be configured to use the new persistence adapter for the personalization feature: -+++ Globally (YAML) -```yaml # config/packages/kreyu_data_table.yaml +::: code-group +```yaml [Globally (YAML)] kreyu_data_table: defaults: personalization: persistence_adapter: app.data_table.personalization.persistence.database ``` -+++ Globally (PHP) -```php # config/packages/kreyu_data_table.php + +```php [Globally (PHP)] use Symfony\Config\KreyuDataTableConfig; return static function (KreyuDataTableConfig $config) { - $defaults = $config->defaults(); - $defaults->personalization() - ->persistenceAdapter('app.data_table.personalization.persistence.database') + $config->defaults() + ->personalization() + ->persistenceAdapter('app.data_table.personalization.persistence.database') ; }; ``` -+++ For data table type -```php # src/DataTable/Type/ProductDataTable.php + +```php [For data table type] use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface; use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; use Symfony\Component\DependencyInjection\Attribute\Autowire; @@ -170,8 +174,8 @@ class ProductDataTableType extends AbstractDataTableType } } ``` -+++ For specific data table -```php # src/Controller/ProductController.php + +```php [For specific data table] use App\DataTable\Type\ProductDataTableType; use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface; @@ -200,7 +204,7 @@ class ProductController extends AbstractController } } ``` -+++ +::: ## Persistence subjects @@ -209,18 +213,24 @@ Persistence subject can be any object that implements `PersistenceSubjectInterfa The value returned in the `getDataTablePersistenceIdentifier()` is used in [persistence adapters](#persistence-adapters) to associate persistent data with the subject. -## Persistence subject providers +### Subject providers Persistence subject providers are classes that allow retrieving the [persistence subjects](#persistence-subjects). Those classes contain `provide` method, that should return the subject, or throw an `PersistenceSubjectNotFoundException`. -### Built-in subject providers +### Built-in token storage subject provider + +The bundle has a built-in token storage subject provider, which uses the [Symfony Security component](https://symfony.com/doc/current/security.html) to retrieve currently logged-in user. +This provider uses the [UserInterface](https://github.com/symfony/symfony/blob/6.4/src/Symfony/Component/Security/Core/User/UserInterface.php) `getUserIdentifier()` +method to retrieve the persistence identifier. -By default, there's only one provider, integrating with Symfony token storage, to retrieve currently logged-in user. -The token storage persistence subject provider uses the [ :icon-mark-github: UserInterface's](https://github.com/symfony/symfony/blob/6.3/src/Symfony/Component/Security/Core/User/UserInterface.php)  -`getUserIdentifier()` method is used as the persistence identifier. If you wish to override this behavior without modifying the `getUserIdentifier()` method, implement the `PersistenceSubjectInterface` on the User entity: +::: danger The persistence identifier must be **unique** per user! +Otherwise, multiple users will override each other's data, like applied filters or current page. +::: -```php # src/Entity/User.php +You can manually provide the persistence identifier by implementing the `PersistenceSubjectInterface` interface on your User entity used by the security: + +```php use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectInterface; class User implements PersistenceSubjectInterface @@ -234,11 +244,18 @@ class User implements PersistenceSubjectInterface } ``` +::: tip Persistence "cache tag contains reserved characters" error? +If your User entity returns email address in `getUserIdentifier()` method, this creates a conflict +when using the [cache adapter](#built-in-cache-adapter), because the `@` character cannot be used as a cache key. + +For more information, see [troubleshooting section](../troubleshooting.md#persistence-cache-tag-contains-reserved-characters-error). +::: + ### Creating custom subject providers To create a custom subject provider, create a class that implements `PersistenceSubjectProviderInterface`: -```php # src/DataTable/Persistence/CustomPersistenceSubjectProvider.php +```php use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface; use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectInterface; @@ -258,7 +275,8 @@ this is already done for you, thanks to [autoconfiguration](https://symfony.com/ When using the default container configuration, that provider should be ready to use. If not, consider tagging this class as `kreyu_data_table.persistence.subject_provider`: -```yaml # config/services.yaml +::: code-group +```yaml [YAML] services: app.data_table.persistence.subject_provider.custom: class: App\DataTable\Persistence\CustomPersistenceSubjectProvider @@ -266,28 +284,42 @@ services: - { 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): +```php [PHP] +use App\DataTable\Persistence\CustomPersistenceSubjectProvider; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + +return static function (ContainerConfigurator $configurator) { + $configurator->services() + ->set('app.data_table.persistence.database', CustomPersistenceSubjectProvider::class) + ->tag('kreyu_data_table.persistence.subject_provider') + ; +} +``` +::: + +The data tables can now be configured to use the new persistence subject provider for any feature. +For example, for personalization feature: -+++ Globally (YAML) -```yaml # config/packages/kreyu_data_table.yaml +::: code-group +```yaml [Globally (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 + +```php [Globally (PHP)] use Symfony\Config\KreyuDataTableConfig; return static function (KreyuDataTableConfig $config) { - $defaults = $config->defaults(); - $defaults->personalization() - ->persistenceSubjectProvider('app.data_table.persistence.subject_provider.custom') + $config->defaults() + ->personalization() + ->persistenceSubjectProvider('app.data_table.persistence.subject_provider.custom') ; }; ``` -+++ For data table type -```php # src/DataTable/Type/ProductDataTable.php + +```php [For data table type] use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface; use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; use Symfony\Component\DependencyInjection\Attribute\Autowire; @@ -309,8 +341,8 @@ class ProductDataTableType extends AbstractDataTableType } } ``` -+++ For specific data table -```php # src/Controller/ProductController.php + +```php [For specific data table] use App\DataTable\Type\ProductDataTableType; use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface; @@ -339,4 +371,35 @@ class ProductController extends AbstractController } } ``` -+++ \ No newline at end of file +::: + +## Persistence clearers + +Persistence data can be cleared using persistence clearers, which are classes that implement [`PersistenceClearerInterface`](#). +Those classes contain a `clear()` method, which accepts a [persistence subject](#persistence-subjects) as an argument. + +Because the bundle has a built-in cache adapter, it also provides a cache persistence clearer: + +```bash +$ bin/console debug:container kreyu_data_table.persistence.clearer.cache +``` + +Let's assume, that the user has a "Clear data table persistence" button, somewhere on the "settings" page. +Handling this button in controller is very straightforward: + +```php +use App\Entity\User; +use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceClearerInterface; +use Symfony\Component\Routing\Annotation\Route; + +class UserController +{ + #[Route('/users/{id}/clear-persistence')] + public function clearPersistence(User $user, PersistenceClearerInterface $persistenceClearer) + { + $persistenceClearer->clear($user); + + // Flash with success, redirect, etc... + } +} +``` diff --git a/docs/features/personalization.md b/docs/src/docs/features/personalization.md old mode 100755 new mode 100644 similarity index 74% rename from docs/features/personalization.md rename to docs/src/docs/features/personalization.md index 9d0352bd..9b4619e4 --- a/docs/features/personalization.md +++ b/docs/src/docs/features/personalization.md @@ -1,23 +1,22 @@ ---- -order: d ---- - # Personalization -![Personalization with the Tabler theme](../static/personalization_modal.png)-- - The data tables can be _personalized_, which can be helpful when working with many columns, by giving the user ability to: - set the priority (order) of the columns; - show or hide specific columns; +::: details Screenshots +![Personalization modal with Tabler theme](/personalization_modal.png) +::: + +[[toc]] + ## Prerequisites To begin with, make sure the [Symfony UX integration is enabled](../installation.md#enable-the-symfony-ux-integration). -Then, enable the **personalization** controller: +Then, enable the **personalization** controller in your `assets/controllers.json` file: -:::flex -```json # assets/controllers.json +```json { "controllers": { "@kreyu/data-table-bundle": { @@ -28,7 +27,6 @@ Then, enable the **personalization** controller: } } ``` -::: ## Toggling the feature @@ -36,15 +34,15 @@ By default, the personalization feature is **disabled** for every data table. You can change this setting globally using the package configuration file, or use `personalization_enabled` option: -+++ Globally (YAML) -```yaml # config/packages/kreyu_data_table.yaml +::: code-group +```yaml [Globally (YAML)] kreyu_data_table: defaults: personalization: enabled: true ``` -+++ Globally (PHP) -```php # config/packages/kreyu_data_table.php + +```php [Globally (PHP)] use Symfony\Config\KreyuDataTableConfig; return static function (KreyuDataTableConfig $config) { @@ -52,8 +50,8 @@ return static function (KreyuDataTableConfig $config) { $defaults->personalization()->enabled(true); }; ``` -+++ For data table type -```php # src/DataTable/Type/ProductDataTable.php + +```php [For data table type] use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -67,8 +65,8 @@ class ProductDataTableType extends AbstractDataTableType } } ``` -+++ For specific data table -```php # src/Controller/ProductController.php + +```php [For specific data table] use App\DataTable\Type\ProductDataTableType; use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -89,16 +87,16 @@ class ProductController extends AbstractController } } ``` -+++ +::: -## Configuring the feature persistence +## Saving applied personalization By default, the personalization feature [persistence](persistence.md) is **disabled** for every data table. You can configure the [persistence](persistence.md) globally using the package configuration file, or its related options: -+++ Globally (YAML) -```yaml # config/packages/kreyu_data_table.yaml +::: code-group +```yaml [Globally (YAML)] kreyu_data_table: defaults: personalization: @@ -108,8 +106,8 @@ kreyu_data_table: # if persistence is enabled and symfony/security-bundle is installed, null otherwise persistence_subject_provider: kreyu_data_table.persistence.subject_provider.token_storage ``` -+++ Globally (PHP) -```php # config/packages/kreyu_data_table.php + +```php [Globally (PHP)] use Symfony\Config\KreyuDataTableConfig; return static function (KreyuDataTableConfig $config) { @@ -123,8 +121,8 @@ return static function (KreyuDataTableConfig $config) { ; }; ``` -+++ For data table type -```php # src/DataTable/Type/ProductDataTable.php + +```php [For data table type] use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface; use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface; use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; @@ -143,13 +141,13 @@ class ProductDataTableType extends AbstractDataTableType $resolver->setDefaults([ 'personalization_persistence_enabled' => true, 'personalization_persistence_adapter' => $this->persistenceAdapter, - 'personalization_persistence_subject' => $this->persistenceSubjectProvider->provide(), + 'personalization_persistence_subject_provider' => $this->persistenceSubjectProvider, ]); } } ``` -+++ For specific data table -```php # src/Controller/ProductController.php + +```php [For specific data table] use App\DataTable\Type\ProductDataTableType; use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface; @@ -174,22 +172,22 @@ class ProductController extends AbstractController options: [ 'personalization_persistence_enabled' => true, 'personalization_persistence_adapter' => $this->persistenceAdapter, - 'personalization_persistence_subject' => $this->persistenceSubjectProvider->provide(), + 'personalization_persistence_subject_provider' => $this->persistenceSubjectProvider, ], ); } } ``` -+++ +::: -## Configuring default personalization +## Default personalization 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 columns [`priority`](../../reference/types/column/column.md#priority), [`visible`](../../reference/types/column/column.md#visible) and [`personalizable`](../../reference/types/column/column.md#personalizable) options (recommended); - using the data table builder's `setDefaultPersonalizationData()` method; -```php # src/DataTable/Type/ProductDataTableType.php +```php src/DataTable/Type/ProductDataTableType.php use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; use Kreyu\Bundle\DataTableBundle\Personalization\PersonalizationData; @@ -230,17 +228,23 @@ class ProductDataTableType extends AbstractDataTableType ## 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: +The following events are dispatched when `personalize()` method of the [`DataTableInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/DataTableInterface.php) is called: -[: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. +::: info PRE_PERSONALIZE +Dispatched before the personalization data is applied to the data table. +Can be used to modify the personalization data, e.g. to dynamically specify priority or visibility of the columns. -[: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. +**See**: [`DataTableEvents::PRE_PERSONALIZE`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php) +::: + +::: info POST_PERSONALIZE +Dispatched after the personalization data is applied to the data table and saved if the personalization persistence is enabled. +Can be used to execute additional logic after the personalization is applied. + +**See**: [`DataTableEvents::POST_PERSONALIZE`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php) +::: -The 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): +The dispatched events are instance of the [`DataTablePersonalizationEvent`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTablePersonalizationEvent.php): ```php use Kreyu\Bundle\DataTableBundle\Event\DataTablePersonalizationEvent; diff --git a/docs/features/sorting.md b/docs/src/docs/features/sorting.md old mode 100755 new mode 100644 similarity index 79% rename from docs/features/sorting.md rename to docs/src/docs/features/sorting.md index 58f58cd9..e38ec251 --- a/docs/features/sorting.md +++ b/docs/src/docs/features/sorting.md @@ -1,10 +1,8 @@ ---- -order: b ---- - # Sorting -The data tables can be _sorted_, which is helpful when working with sortable data sources. +The data tables can be _sorted_ by any defined [column](../components/columns.md). + +[[toc]] ## Toggling the feature @@ -12,15 +10,15 @@ By default, the sorting feature is **enabled** for every data table. You can change this setting globally using the package configuration file, or use `sorting_enabled` option: -+++ Globally (YAML) -```yaml # config/packages/kreyu_data_table.yaml +::: code-group +```yaml [Globally (YAML)] kreyu_data_table: defaults: sorting: enabled: true ``` -+++ Globally (PHP) -```php # config/packages/kreyu_data_table.php + +```php [Globally (PHP)] use Symfony\Config\KreyuDataTableConfig; return static function (KreyuDataTableConfig $config) { @@ -28,8 +26,8 @@ return static function (KreyuDataTableConfig $config) { $defaults->sorting()->enabled(true); }; ``` -+++ For data table type -```php # src/DataTable/Type/ProductDataTable.php + +```php [For data table type] use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -43,8 +41,8 @@ class ProductDataTableType extends AbstractDataTableType } } ``` -+++ For specific data table -```php # src/Controller/ProductController.php + +```php [For specific data table] use App\DataTable\Type\ProductDataTableType; use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -65,17 +63,17 @@ class ProductController extends AbstractController } } ``` -+++ +::: -!!! Enabling the feature does not mean that any column will be sortable by itself. +::: tip Enabling the feature does not mean that any column will be sortable by itself. By default, columns **are not** sortable. -!!! +::: -### Making the columns sortable +## Making the columns sortable To make any column sortable, use its `sort` option: -```php # src/DataTable/Type/ProductDataTableType.php +```php use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; use Kreyu\Bundle\DataTableBundle\Column\Type\NumberColumnType; use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; @@ -96,7 +94,7 @@ class ProductDataTableType extends AbstractDataTableType The bundle will use the column name as the path to perform sorting on. However, if the path is different from the column name (for example, to display "category", but sort by the "category name"), provide it using the same `sort` option: -```php # src/DataTable/Type/ProductDataTableType.php +```php use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType; use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; @@ -117,7 +115,7 @@ 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 +```php use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType; use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; @@ -135,14 +133,14 @@ class ProductDataTableType extends AbstractDataTableType } ``` -## Configuring the feature persistence +## Saving applied sorting By default, the sorting feature [persistence](persistence.md) is **disabled** for every data table. You can configure the [persistence](persistence.md) globally using the package configuration file, or its related options: -+++ Globally (YAML) -```yaml # config/packages/kreyu_data_table.yaml +::: code-group +```yaml [Globally (YAML)] kreyu_data_table: defaults: sorting: @@ -152,8 +150,8 @@ kreyu_data_table: # if persistence is enabled and symfony/security-bundle is installed, null otherwise persistence_subject_provider: kreyu_data_table.persistence.subject_provider.token_storage ``` -+++ Globally (PHP) -```php # config/packages/kreyu_data_table.php + +```php [Globally (PHP)] use Symfony\Config\KreyuDataTableConfig; return static function (KreyuDataTableConfig $config) { @@ -167,8 +165,8 @@ return static function (KreyuDataTableConfig $config) { ; }; ``` -+++ For data table type -```php # src/DataTable/Type/ProductDataTable.php + +```php [For data table type] use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface; use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectProviderInterface; use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; @@ -195,8 +193,8 @@ class ProductDataTableType extends AbstractDataTableType } } ``` -+++ For specific data table -```php # src/Controller/ProductController.php + +```php [For specific data table] use App\DataTable\Type\ProductDataTableType; use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface; @@ -230,9 +228,9 @@ class ProductController extends AbstractController } } ``` -+++ +::: -## Configuring default sorting +## Default sorting The default sorting data can be overridden using the data table builder's `setDefaultSortingData()` method: @@ -258,28 +256,34 @@ class ProductDataTableType extends AbstractDataTableType } ``` -!!! The initial sorting can be performed on multiple columns! +::: tip The initial sorting can be performed on multiple columns! Although, with built-in themes, the user can perform sorting only by a single column. -!!! +::: ## Events -The following events are dispatched when [:icon-mark-github: DataTableInterface::sort()](https://github.com/Kreyu/data-table-bundle/blob/main/src/DataTableInterface.php) is called: +The following events are dispatched when `sort()` method of the [`DataTableInterface`](https://github.com/Kreyu/data-table-bundle/blob/main/src/DataTableInterface.php) is called: + +::: info PRE_SORT +Dispatched before the sorting data is applied to the query. +Can be used to modify the sorting data, e.g. to force sorting by additional column. + +**See**: [`DataTableEvents::PRE_SORT`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php) +::: -[: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. +::: info POST_SORT +Dispatched after the sorting data is applied to the query and saved if the sorting persistence is enabled; +Can be used to execute additional logic after the sorting is applied. -[: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. +**See**: [`DataTableEvents::POST_SORT`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableEvents.php) +::: -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): +The dispatched events are instance of the [`DataTableSortingEvent`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Event/DataTableSortingEvent.php): ```php use Kreyu\Bundle\DataTableBundle\Event\DataTableSortingEvent; -class DataTableExportListener +class DataTableSortingListener { public function __invoke(DataTableSortingEvent $event): void { diff --git a/docs/features/theming.md b/docs/src/docs/features/theming.md old mode 100755 new mode 100644 similarity index 71% rename from docs/features/theming.md rename to docs/src/docs/features/theming.md index c4dcfb27..d55a7b60 --- a/docs/features/theming.md +++ b/docs/src/docs/features/theming.md @@ -1,24 +1,18 @@ ---- -order: h ---- - # Theming Every HTML part of this bundle can be customized using [Twig](https://twig.symfony.com/) themes. +[[toc]] + ## Built-in themes The following themes are natively available in the bundle: -- [:icon-mark-github: @KreyuDataTable/themes/bootstrap_5.html.twig](https://github.com/Kreyu/data-table-bundle/blob/main/src/Resources/views/themes/bootstrap_5.html.twig) - integrates [Bootstrap 5](https://getbootstrap.com/docs/5.0/); -- [:icon-mark-github: @KreyuDataTable/themes/tabler.html.twig](https://github.com/Kreyu/data-table-bundle/blob/main/src/Resources/views/themes/tabler.html.twig) - integrates [Tabler UI Kit](https://tabler.io/); -- [:icon-mark-github: @KreyuDataTable/themes/base.html.twig](https://github.com/Kreyu/data-table-bundle/blob/main/src/Resources/views/themes/base.html.twig) - base HTML template; - -By default, the [:icon-mark-github: @KreyuDataTable/themes/base.html.twig](https://github.com/Kreyu/data-table-bundle/blob/main/src/Resources/views/themes/base.html.twig) theme is used. +- [`@KreyuDataTable/themes/bootstrap_5.html.twig`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Resources/views/themes/bootstrap_5.html.twig) - integrates [Bootstrap 5](https://getbootstrap.com/docs/5.0/); +- [`@KreyuDataTable/themes/tabler.html.twig`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Resources/views/themes/tabler.html.twig) - integrates [Tabler UI Kit](https://tabler.io/); +- [`@KreyuDataTable/themes/base.html.twig`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Resources/views/themes/base.html.twig) - base HTML template; -!!! Note -The default template provides minimal HTML required to properly display the data table. -!!! +By default, the [`@KreyuDataTable/themes/base.html.twig`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Resources/views/themes/base.html.twig) theme is used. ## Selecting a theme @@ -26,15 +20,15 @@ To select a theme, use `themes` option. For example, in order to use the [Bootstrap 5](https://getbootstrap.com/docs/5.0/) theme: -+++ Globally (YAML) -```yaml # config/packages/kreyu_data_table.yaml +::: code-group +```yaml [Globally (YAML)] kreyu_data_table: defaults: themes: - '@KreyuDataTable/themes/bootstrap_5.html.twig' ``` -+++ Globally (PHP) -```php # config/packages/kreyu_data_table.php + +```php [Globally (PHP)] use Symfony\Config\KreyuDataTableConfig; return static function (KreyuDataTableConfig $config) { @@ -43,8 +37,8 @@ return static function (KreyuDataTableConfig $config) { ]); }; ``` -+++ For data table type -```php # src/DataTable/Type/ProductDataTable.php + +```php [For data table type] use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -60,8 +54,8 @@ class ProductDataTableType extends AbstractDataTableType } } ``` -+++ For specific data table -```php # src/Controller/ProductController.php + +```php [For specific data table] use App\DataTable\Type\ProductDataTableType; use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -84,7 +78,7 @@ class ProductController extends AbstractController } } ``` -+++ +::: ## Customizing existing theme @@ -105,15 +99,15 @@ using the built-in themes as a fallback, for example: {% endblock %} ``` -+++ Globally (YAML) -```yaml # config/packages/kreyu_data_table.yaml +::: code-group +```yaml [Globally (YAML)] kreyu_data_table: defaults: themes: - '@KreyuDataTable/themes/bootstrap_5.html.twig' ``` -+++ Globally (PHP) -```php # config/packages/kreyu_data_table.php + +```php [Globally (PHP)] use Symfony\Config\KreyuDataTableConfig; return static function (KreyuDataTableConfig $config) { @@ -123,8 +117,8 @@ return static function (KreyuDataTableConfig $config) { ]); }; ``` -+++ For data table type -```php # src/DataTable/Type/ProductDataTable.php + +```php [For data table type] use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -141,8 +135,8 @@ class ProductDataTableType extends AbstractDataTableType } } ``` -+++ For specific data table -```php # src/Controller/ProductController.php + +```php [For specific data table] use App\DataTable\Type\ProductDataTableType; use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -166,4 +160,4 @@ class ProductController extends AbstractController } } ``` -+++ +::: \ No newline at end of file diff --git a/docs/installation.md b/docs/src/docs/installation.md old mode 100755 new mode 100644 similarity index 54% rename from docs/installation.md rename to docs/src/docs/installation.md index 4fab62c4..0b270f2e --- a/docs/installation.md +++ b/docs/src/docs/installation.md @@ -1,45 +1,45 @@ ---- -icon: rocket -order: 5 ---- - # Installation This bundle can be installed at any moment during a project’s lifecycle. +[[toc]] + +## Prerequisites + +- PHP version 8.1 or higher +- Symfony version 6.0 or higher + ## Download the bundle Use [Composer](https://getcomposer.org/) to install the bundle: ```shell -$ composer require kreyu/data-table-bundle 0.15.* +composer require kreyu/data-table-bundle 0.16.* ``` -!!! Warning +::: danger This bundle is not production ready! It is recommended to lock the minor version, as minor versions can provide breaking changes until the stable release! -!!! - -If your application is using [Symfony Flex](https://github.com/symfony/flex), you can skip to [installing the front-end dependencies](#install-front-end-dependencies-and-rebuild). +::: ## Enable the bundle -Enable the bundle by adding it to the `config/bundles.php`: +Enable the bundle by adding it to the `config/bundles.php` file of your project: -```php # config/bundles.php +```php return [ // ... Kreyu\Bundle\DataTableBundle\KreyuDataTableBundle::class => ['all' => true], ]; ``` -## Enable the Symfony UX integration +## Enable the Stimulus controllers -This bundle provides front-end scripts created using the [Stimulus JavaScript framework](https://stimulus.hotwired.dev/). +This bundle provides front-end scripts created using the [Stimulus JavaScript framework](https://stimulus.hotwired.dev/). To begin with, make sure your application uses the [Symfony Stimulus Bridge](https://github.com/symfony/stimulus-bridge). -Add `@kreyu/data-table-bundle` dependency to your `package.json` file: +Then, add `@kreyu/data-table-bundle` dependency to your `package.json` file: -```json # package.json +```json { "devDependencies": { "@kreyu/data-table-bundle": "file:vendor/kreyu/data-table-bundle/assets" @@ -47,9 +47,9 @@ Add `@kreyu/data-table-bundle` dependency to your `package.json` file: } ``` -Now, add `@kreyu/data-table-bundle` controllers to your `assets/controllers.json` file: +Now, add `@kreyu/data-table-bundle` controllers to your `assets/controllers.json` file: -```json # assets/controllers.json +```json { "controllers": { "@kreyu/data-table-bundle": { @@ -64,18 +64,18 @@ Now, add `@kreyu/data-table-bundle` controllers to your `assets/controllers.json } ``` -## Install front-end dependencies and rebuild +## Rebuild assets -If you're using WebpackEncore, install your assets and restart Encore (not needed if you're using AssetMapper): +If you're using [AssetMapper](https://symfony.com/doc/current/frontend.html#assetmapper-recommended), you're good to go. Otherwise, run following command: -+++ yarn -```shell -$ yarn install --force -$ yarn watch +::: code-group + +```shell [yarn] +yarn install --force && yarn watch ``` -+++ npm -```shell -$ npm install --force -$ npm run watch + +```shell [npm] +npm install --force && npm run watch ``` -+++ \ No newline at end of file + +::: diff --git a/docs/src/docs/integrations/doctrine-orm/events.md b/docs/src/docs/integrations/doctrine-orm/events.md new file mode 100644 index 00000000..c188f9f0 --- /dev/null +++ b/docs/src/docs/integrations/doctrine-orm/events.md @@ -0,0 +1,75 @@ +# Events + +The Doctrine ORM filter handler dispatches two events, which can be used to modify the query parameters and the expression. + +## PreSetParametersEvent + +The [PreSetParametersEvent](../src/Event/PreSetParametersEvent.php) is dispatched before `$queryBuilder->setParameter()` +is called for every parameter required by the filter. It can be used to modify the parameters, before they are passed to the query builder. + +```php +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Event\DoctrineOrmFilterEvents; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Event\PreSetParametersEvent; + +class ProductDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + // ... + + $builder + ->getFilter('name') + ->addEventListener(DoctrineOrmFilterEvents::PRE_SET_PARAMETERS, function (PreSetParametersEvent $event) { + $filter = $event->getFilter(); + $data = $event->getData(); + $query = $event->getQuery(); + $parameters = $event->getParameters(); + + // ... + + $event->setParameters($parameters); + }); + } +} +``` + +## PreApplyExpressionEvent + +The [PreApplyExpressionEvent](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Event/PreApplyExpressionEvent.php) is dispatched before `$queryBuilder->andWhere()` is called. +It can be used to modify the expression before it is passed to the query builder. + +::: tip Use [expression transformers](expression-transformers.md) for easier and reusable solution for modifying the expression. +Those transformers are called by the [ApplyExpressionTransformers](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/EventListener/ApplyExpressionTransformers.php) event subscriber, +which is automatically used in [DoctrineOrmFilterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/Type/DoctrineOrmFilterType.php) filter type, as well as +every filter type that uses it as a parent. +::: + +```php +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Event\DoctrineOrmFilterEvents; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Event\PreApplyExpressionEvent; + +class ProductDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + // ... + + $builder + ->getFilter('name') + ->addEventListener(DoctrineOrmFilterEvents::PRE_APPLY_EXPRESSION, function (PreApplyExpressionEvent $event) { + $filter = $event->getFilter(); + $data = $event->getData(); + $query = $event->getQuery(); + $expression = $event->getExpression(); + + // ... + + $event->setExpression($expression); + }); + } +} +``` \ No newline at end of file diff --git a/docs/src/docs/integrations/doctrine-orm/expression-transformers.md b/docs/src/docs/integrations/doctrine-orm/expression-transformers.md new file mode 100644 index 00000000..3f3964e1 --- /dev/null +++ b/docs/src/docs/integrations/doctrine-orm/expression-transformers.md @@ -0,0 +1,241 @@ +# Expression transformers + +Expression transformers provide a way to extend Doctrine DQL expressions before they are executed by a filter handler. + +[[toc]] + +## Built-in expression transformers + +- `TrimExpressionTransformer` - wraps the expression in the `TRIM()` function +- `LowerExpressionTransformer` - wraps the expression in the `LOWER()` function +- `UpperExpressionTransformer` - wraps the expression in the `UPPER()` function +- `CallbackExpressionTransformer` - allows transforming the expression using the callback function + +The expression transformers can be passed using the `expression_transformers` option: + +```php +use App\DataTable\Filter\ExpressionTransformer\UnaccentExpressionTransformer; +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\ExpressionTransformer\LowerExpressionTransformer; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\ExpressionTransformer\TrimExpressionTransformer; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType; + +class ProductDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + $builder + ->addFilter('name', TextFilterType::class, [ + 'expression_transformers' => [ + new LowerExpressionTransformer(), + new TrimExpressionTransformer(), + ], + ]) + ; + } +} +``` + +
+ +The transformers are called in the same order as they are passed. + +
+ +For easier usage, some of the built-in transformers can be enabled using the `trim`, `lower` and `upper` filter options: + +```php +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType; + +class ProductDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + $builder + ->addFilter('name', TextFilterType::class, [ + 'trim' => true, + 'lower' => true, + 'upper' => true, + ]) + ; + } +} +``` + +
+ +When using the `trim`, `lower` or `upper` options, their transformers are called **before** the `expression_transformers` ones. + +
+ +## Creating a custom expression transformer + +To create a custom expression transformer, create a new class that implements `ExpressionTransformerInterface`: + +```php +namespace App\DataTable\Filter\ExpressionTransformer; + +use Doctrine\ORM\Query\Expr; +use Doctrine\ORM\Query\Expr\Comparison; +use Kreyu\Bundle\DataTableBundle\Exception\UnexpectedTypeException; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\ExpressionTransformer\ExpressionTransformerInterface; + +class UnaccentExpressionTransformer implements ExpressionTransformerInterface +{ + public function transform(mixed $expression): mixed + { + if (!$expression instanceof Comparison) { + throw new UnexpectedTypeException($expression, Comparison::class); + } + + $leftExpr = sprintf('UNACCENT(%s)', (string) $expression->getLeftExpr()); + $rightExpr = sprintf('UNACCENT(%s)', (string) $expression->getRightExpr()); + + // or use expression API: + // + // $leftExpr = new Expr\Func('UNACCENT', $expression->getLeftExpr()); + // $rightExpr = new Expr\Func('UNACCENT', $expression->getRightExpr()); + + return new Comparison($leftExpr, $expression->getOperator(), $rightExpr); + } +} +``` + +If you're sure that the expression transformer requires the expression to be a comparison (it will be in most cases), +you can extend the `AbstractComparisonExpressionTransformer` class which simplifies the definition: + +```php +namespace App\DataTable\Filter\ExpressionTransformer; + +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\ExpressionTransformer\AbstractComparisonExpressionTransformer; + +class UnaccentExpressionTransformer extends AbstractComparisonExpressionTransformer +{ + protected function transformLeftExpr(mixed $leftExpr): mixed + { + return sprintf('UNACCENT(%s)', (string) $leftExpr); + + // or use expression API: + // + // return new Expr\Func('UNACCENT', $leftExpr); + } + + protected function transformRightExpr(mixed $rightExpr): mixed + { + return sprintf('UNACCENT(%s)', (string) $rightExpr); + + // or use expression API: + // + // return new Expr\Func('UNACCENT', $rightExpr); + } +} +``` + +The `AbstractComparisonExpressionTransformer` accepts two boolean arguments: + +- `transformLeftExpr` - defaults to `true` +- `transformRightExpr` - defaults to `true` + +Thanks to that, the user can specify which side of the expression should be transformed. +The `transformLeftExpr()` and `transformRightExpr()` methods are called only when necessary. For example: + +```php +$expression = new Expr\Comparison('foo', '=', 'bar'); + +// LOWER(foo) = LOWER(bar) +(new LowerExpressionTransformer()) + ->transform($expression); + +// foo = LOWER(bar) +(new LowerExpressionTransformer(transformLeftExpr: false, transformRightExpr: true)) + ->transform($expression); + +// LOWER(foo) = bar +(new LowerExpressionTransformer(transformLeftExpr: true, transformRightExpr: false)) + ->transform($expression); +``` + +To use the created expression transformer, pass it as the `expression_transformers` filter type option: + +```php +use App\DataTable\Filter\ExpressionTransformer\UnaccentExpressionTransformer; +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType; + +class ProductDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + $builder + ->addFilter('name', TextFilterType::class, [ + 'expression_transformers' => [ + new UnaccentExpressionTransformer(), + ], + ]) + ; + } +} +``` + +## Adding an option to automatically apply transformer + +Following the above example of `UnaccentExpressionTransformer`, let's assume, that we want to create such definition: + +```php +use App\DataTable\Filter\ExpressionTransformer\UnaccentExpressionTransformer; +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType; + +class ProductDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + $builder + ->addFilter('name', TextFilterType::class, [ + 'unaccent' => true, + ]) + ; + } +} +``` + +To achieve that, create a custom filter type extension: + +```php +use App\DataTable\Filter\ExpressionTransformer\UnaccentExpressionTransformer; +use Kreyu\Bundle\DataTableBundle\Filter\Extension\AbstractFilterTypeExtension; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\DoctrineOrmFilterType; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\OptionsResolver\Options; + +class UnaccentFilterTypeExtension extends AbstractFilterTypeExtension +{ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setDefault('unaccent', false) + ->setAllowedTypes('unaccent', 'bool') + ->addNormalizer('expression_transformers', function (Options $options, array $expressionTransformers) { + if ($options['unaccent']) { + $expressionTransformers[] = new UnaccentExpressionTransformer(); + } + + return $expressionTransformers; + }) + ; + } + + public static function getExtendedTypes(): iterable + { + return [DoctrineOrmFilterType::class]; + } +} +``` + +The `unaccent` option is now defined, and defaults to `false`. In addition, the options resolver normalizer will automatically push an instance +of the custom expression transformer to the `expression_transformers` option, but only if the `unaccent` option equals `true`. diff --git a/docs/src/docs/introduction.md b/docs/src/docs/introduction.md new file mode 100644 index 00000000..a9d240e2 --- /dev/null +++ b/docs/src/docs/introduction.md @@ -0,0 +1,106 @@ +# Introduction + +DataTableBundle is a Symfony bundle that aims to help with data tables. + +[[toc]] + +
+ +Just want to try it out? Skip to the [installation](installation.md). + +
+ +## Features + +- [Type classes](#similarity-to-form-component) for a class-based configuration, like in a Symfony Form component +- [Sorting](features/sorting.md), [filtering](features/filtering.md) and [pagination](features/pagination.md) - triforce of the data tables +- [Personalization](features/personalization.md) where the user decides the order and visibility of columns +- [Persistence](features/persistence.md) to save applied data (e.g. filters) between requests +- [Exporting](features/exporting.md) with or without applied pagination, filters and personalization +- [Theming](features/theming.md) of every part of the bundle using Twig +- [Data source agnostic](features/extensibility.md) with optional Doctrine ORM integration bundle +- [Integration with Hotwire Turbo](features/asynchronicity.md) for asynchronicity + +## Use cases + +If your application uses an admin panel generator, like a SonataAdminBundle or EasyAdminBundle, you won't need this bundle. +Those generators already cover the definition of data tables in their own way. + +However, if your application is complex enough that a simple panel generator is not enough, for example, a very specific B2B or CRM platform, +you may consider DataTableBundle, which focuses solely on the data tables, like a Form component focuses solely on the forms. +It can save a lot of time when compared to rendering tables manually (especially with filters), and helps with keeping visual homogeneity. + +## Similarity to form component + +Everything is designed to be friendly to a Symfony developers that used the Symfony Form component before. +Data tables and their components - [columns](components/columns.md), [filters](components/filters.md), [actions](components/actions.md) and [exporters](components/exporters.md), are defined using type classes, like a forms: + +```php +class ProductDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + $builder + ->addColumn('id', NumberColumnType::class) + ->addColumn('name', TextColumnType::class); + + $builder + ->addFilter('id', NumberFilterType::class) + ->addFilter('name', TextFilterType::class); + + $builder + ->addAction('create', ButtonActionType::class) + ->addRowAction('update', ButtonActionType::class) + ->addBatchAction('delete', ButtonActionType::class); + + $builder + ->addExporter('csv', CsvExporterType::class) + ->addExporter('xlsx', XlsxExporterType::class); + } +} +``` + +Creating the data tables using those type classes may also seem very familiar: + +```php +class ProductController extends AbstractController +{ + use DataTableFactoryAwareTrait; + + public function index(Request $request): Response + { + $dataTable = $this->createDataTable(ProductDataTableType::class, $query); + $dataTable->handleRequest($request); + + return $this->render('product/index.html.twig', [ + 'products' => $dataTable->createView(), + ]) + } +} +``` + +Rendering the data table in Twig is as simple as executing a single function: + +```twig +{# templates/product/index.html.twig #} +
+ {{ data_table(products, { title: 'Products' }) }} +
+``` + +## Recommended practices + +When working with Form component, each "Type" refers to the form type. + +When working with DataTableBundle, there are many new and different types - data table, column, filter, action and exporter types. + +For readability, it is recommended to name form types with `FormType` suffix, instead of a simple `Type`. +This makes a context of the type class clear: + +- `ProductFormType` - defines product form; +- `ProductDataTableType` - defines product list; +- `ProductColumnType` - defines product column (if separate definition is needed); +- `ProductFilterType` - defines product filter (if separate definition is needed); +- etc. + +Also, type extensions - instead of `TypeExtension`, use `FormTypeExtension` suffix. diff --git a/docs/troubleshooting.md b/docs/src/docs/troubleshooting.md old mode 100755 new mode 100644 similarity index 78% rename from docs/troubleshooting.md rename to docs/src/docs/troubleshooting.md index 0fe0670e..e078b5c8 --- a/docs/troubleshooting.md +++ b/docs/src/docs/troubleshooting.md @@ -1,12 +1,9 @@ ---- -icon: bug -order: 2 ---- - # Troubleshooting This section covers common problems and how to fix them. +[[toc]] + ## Pagination is enabled but its controls are not visible By default, every data table has pagination feature _enabled_. @@ -14,12 +11,12 @@ However, if you don't see the pagination controls, make sure your data table has With default pagination data, every page contains 25 records. Built-in themes display pagination controls only when the data table contains more than one page. -Also, remember that you can [change the default pagination data](features/pagination.md#configuring-default-pagination), reducing the per-page limit. +Also, remember that you can [change the default pagination data](features/pagination.md#default-pagination), reducing the per-page limit. For more information, consider reading: -- [Features > Pagination > Toggling the feature](features/pagination.md#toggling-the-feature) -- [Features > Pagination > Configuring the default pagination](features/pagination.md#configuring-default-pagination) +- Features › Pagination › [Toggling the feature](features/pagination.md#toggling-the-feature) +- Features › Pagination › [Default pagination](features/pagination.md#default-pagination) ## Sorting is enabled but columns are not sortable @@ -28,8 +25,7 @@ By default, columns **are not** sortable. To make a column sortable, use its `so For more information, consider reading: -- [Basic Usage > Adding columns > Making the columns sortable](basic-usage/adding-columns.md#making-the-columns-sortable) -- [Features > Sorting > Making the columns sortable](features/sorting.md#making-the-columns-sortable) +- Features › Sorting › [Making the columns sortable](features/sorting.md#making-the-columns-sortable) ## Exporting is enabled but exported files are empty @@ -38,8 +34,7 @@ By default, columns **are not** sortable. To make a column exportable, use its ` For more information, consider reading: -- [Basic Usage > Adding exporters > Making the columns exportable](basic-usage/exporting-the-data.md#making-the-columns-exportable) -- [Features > Exporting > Making the columns exportable](features/exporting.md#making-the-columns-exportable) +- Features › Exporting › [Making the columns exportable](features/exporting.md#making-the-columns-exportable) ## Data table features are refreshing the page but not working @@ -51,7 +46,7 @@ If, for example, a data table is rendered properly, but: refreshes the page but does nothing else, make sure you handled the request using the `handleRequest()` method: -```php #13 src/Controller/ProductController.php +```php use App\DataTable\Type\ProductDataTableType; use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -69,20 +64,15 @@ class ProductController extends AbstractController } ``` -For more information, consider reading: - -- [Basic Usage > Rendering the table > Binding request to the data table](basic-usage/rendering-the-table.md#binding-request-to-the-data-table) -- [Features > Request handlers](features/request-handlers.md) - ## The N+1 problems -This section covers common issues with N+1 queries when working with Doctrine ORM. +This section covers common issues with N+1 queries when working with Doctrine ORM. ### The N+1 problem with relation columns When using Doctrine ORM, if your data table contains columns with data from relationship: -```php # src/DataTable/Type/ProductDataTableType.php +```php use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType; use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; @@ -100,7 +90,7 @@ class ProductDataTableType extends AbstractDataTableType ...then, remember to join and select the association to prevent N+1 queries: -```php # src/Controller/ProductController.php +```php use App\DataTable\Type\ProductDataTableType; use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -130,7 +120,7 @@ If your entity contains a one-to-one relationship that is **not** used in the da the additional queries will be generated anyway, because the [Doctrine Paginator](https://www.doctrine-project.org/projects/doctrine-orm/en/2.15/tutorials/pagination.html) is **always** loading them. To prevent that, add a hint to force a partial load: -```php # src/DataTable/Type/ProductDataTableType.php +```php use Doctrine\ORM\Query; use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQuery; @@ -149,10 +139,10 @@ class ProductDataTableType extends AbstractDataTableType } ``` -!!!warning Warning -Forcing a partial load comes at a price because it changes the entity behavior. -For example, not specifying all used associations in the query's `SELECT` will end up in an error. -!!! +::: warning +Forcing a partial load disables lazy loading, therefore, not specifying +every single association used in the query's `SELECT` will end up in an error. +::: For more information, consider reading: @@ -171,7 +161,7 @@ Unfortunately, in some applications, it may return something with a reserved cha To prevent that, implement a `PersistenceSubjectInterface` interface on the User object and manually return the **unique** identifier: -```php # src/Entity/User.php +```php use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceSubjectInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -188,4 +178,4 @@ class User implements UserInterface, PersistenceSubjectInterface For more information, consider reading: -- [Features > Persistence > Built-in subject providers](features/persistence.md#built-in-subject-providers) +- Features › Persistence › [Subject providers](features/persistence.md#subject-providers) diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 00000000..bdc22f59 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,45 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: "DataTableBundle" + text: for Symfony + tagline: Streamlines creation process of the data tables + image: + src: /logo.png + alt: DataTableBundle + actions: + - theme: brand + text: Documentation + link: /docs/introduction + - theme: alt + text: Reference + link: /reference/types/data-table + +features: + - title: Type classes + details: Class-based configuration, like in a Symfony Form component + - title: Sorting, filtering, pagination + details: Classic triforce of the data tables + - title: Personalization + details: User decides the order and visibility of columns + - title: Persistence + details: Saving applied data (e.g. filters) between requests + - title: Exporting + details: Popular formats, with or without applied pagination, filters and personalization - you name it + - title: Theming + details: Every template part of the bundle is customizable using Twig + - title: Data source agnostic + details: With Doctrine ORM supported out of the box + - title: Asynchronous + details: Thanks to integration with Hotwire Turbo, data tables are asynchronous +--- + + + \ No newline at end of file diff --git a/docs/static/action_confirmation_modal.png b/docs/src/public/action_confirmation_modal.png old mode 100755 new mode 100644 similarity index 100% rename from docs/static/action_confirmation_modal.png rename to docs/src/public/action_confirmation_modal.png diff --git a/docs/static/export_modal.png b/docs/src/public/export_modal.png old mode 100755 new mode 100644 similarity index 100% rename from docs/static/export_modal.png rename to docs/src/public/export_modal.png diff --git a/docs/src/public/logo.png b/docs/src/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d47d7cd7518e7e0da61baf20d5aa0b0662cc66f8 GIT binary patch literal 30120 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 literal 0 HcmV?d00001 diff --git a/docs/static/personalization_modal.png b/docs/src/public/personalization_modal.png old mode 100755 new mode 100644 similarity index 100% rename from docs/static/personalization_modal.png rename to docs/src/public/personalization_modal.png diff --git a/docs/static/global_search.png b/docs/src/public/search_filter_type.png old mode 100755 new mode 100644 similarity index 100% rename from docs/static/global_search.png rename to docs/src/public/search_filter_type.png diff --git a/docs/reference/configuration.md b/docs/src/reference/configuration.md old mode 100755 new mode 100644 similarity index 85% rename from docs/reference/configuration.md rename to docs/src/reference/configuration.md index 72b2ac67..63aa8cd8 --- a/docs/reference/configuration.md +++ b/docs/src/reference/configuration.md @@ -2,24 +2,21 @@ This bundle can be configured using the: -- `config/packages/kreyu_data_table.yaml` (when using YAML configuration); -- `config/packages/kreyu_data_table.php` (when using PHP configuration); +- `config/packages/kreyu_data_table.yaml` when using YAML configuration; +- `config/packages/kreyu_data_table.php` when using PHP configuration; ## Data table builder defaults You can specify default values applied to **all the data tables** using the `defaults` node. -Those are used as a default builder values, unless the user enters some option value manually, -either by passing it as a data table option, or by using the data table builder directly. -!!! Note -The default configuration is loaded by the [:icon-mark-github: DefaultConfigurationDataTableTypeExtension](https://github.com/Kreyu/data-table-bundle/blob/main/src/Extension/Core/DefaultConfigurationDataTableTypeExtension.php), -that extends every data table type class with [:icon-mark-github: DataTableType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Type/DataTableType.php) specified as a parent. -!!! +The defaults are loaded by the [DefaultConfigurationDataTableTypeExtension](https://github.com/Kreyu/data-table-bundle/blob/main/src/Extension/Core/DefaultConfigurationDataTableTypeExtension.php), +which extends every data table type class with [DataTableType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Type/DataTableType.php) specified as a parent. The given values represent the default ones, unless specifically stated otherwise: -+++ YAML -```yaml # config/packages/kreyu_data_table.yaml +::: code-group +```yaml [YAML] +# config/packages/kreyu_data_table.yaml kreyu_data_table: defaults: themes: @@ -62,9 +59,9 @@ kreyu_data_table: form_factory: form.factory exporter_factory: kreyu_data_table.exporter.factory ``` -+++ PHP -```php # config/packages/kreyu_data_table.php - -``` - ### `data_table_table` -**With arguments**: `data_table_table(data_table_view, variables)` +**Usage**: `data_table_table(data_table_view, variables)` Renders the HTML of the data table. ### `data_table_action_bar` -**With arguments**: `data_table_action_bar(data_table_view, variables)` +**Usage**: `data_table_action_bar(data_table_view, variables)` Renders the HTML of the data table action bar, which includes filtration, exporting and personalization features. ### `data_table_header_row` -**With arguments**: `data_table_header_row(header_row_view, variables)` +**Usage**: `data_table_header_row(header_row_view, variables)` Renders the header row of the data table. ### `data_table_value_row` -**With arguments**: `data_table_value_row(value_row_view, variables)` +**Usage**: `data_table_value_row(value_row_view, variables)` Renders the value row of the data table. ### `data_table_column_label` -**With arguments**: `data_table_column_label(column_view, variables)` +**Usage**: `data_table_column_label(column_view, variables)` Renders the label of the column. This takes care of all the label translation logic under the hood. ### `data_table_column_header` -**With arguments**: `data_table_column_header(column_view, variables)` +**Usage**: `data_table_column_header(column_view, variables)` Renders the header of the column. Internally, this does the same as `data_table_column_label()` method, but additionally handles the sorting feature. ### `data_table_column_value` -**With arguments**: `data_table_column_value(column_view, variables)` +**Usage**: `data_table_column_value(column_view, variables)` Renders the value of the column. It handles all the required logic to extract value from the row data based on the column configuration (e.g. to display formatted `name` of the `Project` entity). ### `data_table_filters_form` -**With arguments**: `data_table_filters_form(form, variables)` +**Usage**: `data_table_filters_form(form, variables)` Renders the filters form. Accepts both the `FormInterface` and `FormView`. If given value is instance of `FormInterface`, the `createView()` method will be called. ### `data_table_personalization_form` -**With arguments**: `data_table_personalization_form(form, variables)` +**Usage**: `data_table_personalization_form(form, variables)` Renders the personalization form. Accepts both the `FormInterface` and `FormView`. If given value is instance of `FormInterface`, the `createView()` method will be called. ### `data_table_export_form` -**With arguments**: `data_table_export_form(form, variables)` +**Usage**: `data_table_export_form(form, variables)` Renders the export form. Accepts both the `FormInterface` and `FormView`. If given value is instance of `FormInterface`, the `createView()` method will be called. ### `data_table_pagination` -**With arguments**: `data_table_pagination(pagination_view, variables)` +**Usage**: `data_table_pagination(pagination_view, variables)` Renders the pagination controls. @@ -111,6 +98,10 @@ In this case, the pagination view is extracted from the data table view "paginat Certain types may define even more variables, and some variables here only really apply to certain types. To know the exact variables available for each type, check out the code of the templates used by your data table theme. +::: warning The type classes are constantly changing before the stable release! +Check source code of the type class to make sure of the variables exposed to the template. +::: + ### Data table variables The following variables are common to every data table type: @@ -220,8 +211,8 @@ The following variables are common to every action type: | `icon_attr` | An array of attributes used in rendering the action icon. | | `confirmation` | An array of action confirmation options. If `false`, action is not confirmable. | -!!! Note +::: tip Behind the scenes, these variables are made available to the `DataTableView`, `ColumnView` and `FilterView` objects of your data table when the DataTable component calls `buildView()`. To see what "view" variables a particular type has, find the source code for the used type class and look for the `buildView()` method. -!!! +::: diff --git a/docs/src/reference/types/action.md b/docs/src/reference/types/action.md new file mode 100644 index 00000000..8573bfe8 --- /dev/null +++ b/docs/src/reference/types/action.md @@ -0,0 +1,8 @@ +# Action types + +The following action types are natively available in the bundle: + +- [Link](#) +- [Button](#) +- [Callback](#) +- [Action](#) diff --git a/docs/src/reference/types/action/action.md b/docs/src/reference/types/action/action.md new file mode 100644 index 00000000..2ab71a04 --- /dev/null +++ b/docs/src/reference/types/action/action.md @@ -0,0 +1,11 @@ + + +# ActionType + +The [`ActionType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/ActionType.php) represents a base action, used as a parent for every other action type in the bundle. + +## Options + + diff --git a/docs/reference/actions/types/button.md b/docs/src/reference/types/action/button.md old mode 100755 new mode 100644 similarity index 53% rename from docs/reference/actions/types/button.md rename to docs/src/reference/types/action/button.md index 5e22aea8..84111f51 --- a/docs/reference/actions/types/button.md +++ b/docs/src/reference/types/action/button.md @@ -1,19 +1,10 @@ ---- -label: Button -order: b -tags: - - actions ---- + -# Button action type +# ButtonActionType -The `ButtonActionType` represents an action displayed as a button. - -+-------------+---------------------------------------------------------------------+ -| Parent type | [LinkActionType](link.md) -+-------------+---------------------------------------------------------------------+ -| Class | [:icon-mark-github: ButtonActionType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/ButtonActionType.php) -+-------------+---------------------------------------------------------------------+ +The [`ButtonActionType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/ButtonActionType.php) represents an action rendered as a button. ## Options @@ -34,10 +25,6 @@ $builder ; ``` -!!! Note -The action confirmation configuration inherits value of this option as its `href`. -!!! - ### `target` - **type**: `string` or `callable` @@ -57,4 +44,4 @@ $builder ## Inherited options -{{ include '_action_options' }} + diff --git a/docs/reference/actions/types/form.md b/docs/src/reference/types/action/form.md old mode 100755 new mode 100644 similarity index 57% rename from docs/reference/actions/types/form.md rename to docs/src/reference/types/action/form.md index af2abd04..6409d693 --- a/docs/reference/actions/types/form.md +++ b/docs/src/reference/types/action/form.md @@ -1,25 +1,16 @@ ---- -label: Form -order: c -tags: - - actions ---- + -# Form action type +# FormActionType -The `FormActionType` represents an action displayed as a button. - -+-------------+---------------------------------------------------------------------+ -| Parent type | [ActionType](action.md) -+-------------+---------------------------------------------------------------------+ -| Class | [:icon-mark-github: FormActionType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/FormActionType.php) -+-------------+---------------------------------------------------------------------+ +The [`FormActionType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/FormActionType.php) represents an action rendered as a submit button to a hidden form, which allows the action to use any HTTP method. ## Options ### `action` -- **type**: `string` or `callable` +- **type**: `string` or `callable` - **default**: `'#'` Sets the value that will be used as a form's `action` attribute. @@ -36,7 +27,7 @@ $builder ### `method` -- **type**: `string` or `callable` +- **type**: `string` or `callable` - **default**: `'GET'` Sets the value that will be used as a form's `method` attribute. @@ -53,7 +44,7 @@ $builder ### `button_attr` -- **type**: `array` +- **type**: `array` - **default**: `[]` An array of attributes used to render the form submit button. @@ -72,4 +63,4 @@ $builder ## Inherited options -{{ include '_action_options' }} + diff --git a/docs/src/reference/types/action/link.md b/docs/src/reference/types/action/link.md new file mode 100644 index 00000000..7b158961 --- /dev/null +++ b/docs/src/reference/types/action/link.md @@ -0,0 +1,47 @@ + + +# LinkActionType + +The [`LinkActionType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Action/Type/LinkActionType.php) represents an action rendered as a simple link. + +## Options + +### `href` + +- **type**: `string` or `callable` +- **default**: `'#'` + +A value used as an action link [href attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-href). + +```php # +use Kreyu\Bundle\DataTableBundle\Action\Type\LinkActionType; + +$builder + ->addAction('back', LinkActionType::class, [ + 'href' => $this->urlGenerator->generate('category_index'), + ]) +; +``` + +### `target` + +- **type**: `string` or `callable` +- **default**: `'_self'` + +Sets the value that will be used as an anchor [target attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target). + +```php # +use Kreyu\Bundle\DataTableBundle\Action\Type\LinkActionType; + +$builder + ->addAction('wiki', LinkActionType::class, [ + 'target' => '_blank', + ]) +; +``` + +## Inherited options + + diff --git a/docs/reference/actions/types/_action_options.md b/docs/src/reference/types/action/options/action.md old mode 100755 new mode 100644 similarity index 58% rename from docs/reference/actions/types/_action_options.md rename to docs/src/reference/types/action/options/action.md index 86958f4b..21ac83c2 --- a/docs/reference/actions/types/_action_options.md +++ b/docs/src/reference/types/action/options/action.md @@ -1,9 +1,15 @@ ### `label` -- **type**: `string` or `Symfony\Component\Translation\TranslatableInterface` -- **default**: the label is "guessed" from the action name +- **type**: `null`, `string` or `Symfony\Component\Translation\TranslatableInterface` +- **default**: `null` A label representing the action. +When value equals `null`, a sentence cased action name is used as a label, for example: + +| Action name | Guessed label | +|--------------|----------------| +| create | Create | +| saveAndClose | Save and close | ### `label_translation_parameters` @@ -14,15 +20,15 @@ An array of parameters used to translate the action label. ### `translation_domain` -- **type**: `false` or `string` -- **default**: the default `KreyuDataTable` is used +- **type**: `false` or `string` +- **default**: `'KreyuDataTable'` Translation domain used in translation of action's translatable values. ### `block_prefix` -- **type**: `string` -- **default**: action type block prefix +- **type**: `string` +- **default**: value returned by the action type `getBlockPrefix()` method Allows you to add a custom block prefix and override the block name used to render the action type. Useful, for example, if you have multiple instances of the same action type, and you need to personalize @@ -35,7 +41,7 @@ the rendering of some of them, without the need to create a new action type. Determines whether the action should be visible to the user. -When action is used in the [row context](../../../features/actions/row-actions.md), the callable receives the row data as the first argument: +The callable can only be used by the row actions to determine visibility [based on the row data](../../../../docs/components/actions.md#using-row-data-in-options): ```php use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; @@ -49,13 +55,9 @@ $builder ; ``` -!!!warning Warning -When action is used in the [global](../../../features/actions/global-actions.md) or [batch](../../../features/actions/batch-actions.md) context, the callable receives no arguments! -!!! - ### `attr` -- **type**: `array` +- **type**: `array` - **default**: `[]` An array of attributes used to render the action. @@ -99,37 +101,35 @@ $builder Determines whether the action is confirmable, which displays a modal where user have to acknowledge the process. The modal can be configured by passing an array with the following options: -#### `translation_domain` - -- **type**: `false` or `string` -- **default**: `'KreyuDataTable'` - -Translation domain used in translation of action confirmation labels. - -#### `label_title` - -- **type**: `null` or `string` -- **default**: `'Action confirmation'` - -#### `label_description` - -- **type**: `null` or `string` -- **default**: `'Are you sure you want to execute this action?'` - -#### `label_confirm` - -- **type**: `null` or `string` -- **default**: `'Confirm'` - -#### `label_cancel` - -- **type**: `null` or `string` -- **default**: `'Cancel'` - -#### `type` - -- **type**: `null` or `string` -- **default**: `danger` -- **allowed values**: `danger`, `warning`, `info` - -Represents a type of the action confirmation, which determines the color of the displayed modal. +> #### `translation_domain` +> +> - **type**: `false` or `string` +> - **default**: `'KreyuDataTable'` +> +> #### `label_title` +> +> - **type**: `null` or `string` +> - **default**: `'Action confirmation'` +> +> #### `label_description` +> +> - **type**: `null` or `string` +> - **default**: `'Are you sure you want to execute this action?'` +> +> #### `label_confirm` +> +> - **type**: `null` or `string` +> - **default**: `'Confirm'` +> +> #### `label_cancel` +> +> - **type**: `null` or `string` +> - **default**: `'Cancel'` +> +> #### `type` +> +> - **type**: `null` or `string` +> - **default**: `danger` +> - **allowed values**: `danger`, `warning`, `info` +> +> Represents a type of the action confirmation, which determines the color of the displayed modal. diff --git a/docs/src/reference/types/column.md b/docs/src/reference/types/column.md new file mode 100644 index 00000000..40463fb6 --- /dev/null +++ b/docs/src/reference/types/column.md @@ -0,0 +1,20 @@ +# Column types + +The following column types are natively available in the bundle: + +- Text columns + - [Text](column/text.md) + - [Number](column/number.md) + - [Money](column/money.md) + - [Boolean](column/boolean.md) + - [Link](column/link.md) +- Date and time columns + - [DateTime](column/date-time.md) + - [DatePeriod](column/date-period.md) +- Special columns + - [Collection](column/collection.md) + - [Template](column/template.md) + - [Actions](column/actions.md) + - [Checkbox](column/checkbox.md) +- Base columns + - [Column](column/column.md) \ No newline at end of file diff --git a/docs/src/reference/types/column/actions.md b/docs/src/reference/types/column/actions.md new file mode 100644 index 00000000..4d30a59e --- /dev/null +++ b/docs/src/reference/types/column/actions.md @@ -0,0 +1,74 @@ + + +# ActionsColumnType + +The [`ActionsColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/ActionsColumnType.php) represents a column that contains row actions. + +::: info In most cases, it is not necessary to use this column type directly. +Instead, use data table builder's `addRowAction()` method. +If at least one row action is defined and is visible, an `ActionColumnType` is added to the data table. +::: + +## Options + +### `actions` + +- **type**: `array` +- **default**: `[]` + +This option contains a list of actions. Each action consists of _three_ options: + +> #### `type` +> +> - **type**: `string` +> +> Fully qualified class name of the [action type](#). +>

+> +> #### `type_options` +> +> - **type**: `array` +> - **default**: `[]` +> +> Options passed to the action type. +>

+> +> #### `visible` +> +> - **type**: `bool`or `callable` +> - **default**: `true` + +Determines whether the action should be visible. + +Example usage: + +```php +use Kreyu\Bundle\DataTableBundle\Action\Type\ButtonActionType; +use Kreyu\Bundle\DataTableBundle\Column\Type\ActionsColumnType; + +$builder + ->addColumn('actions', ActionsColumnType::class, [ + 'actions' => [ + 'show' => [ + 'type' => ButtonActionType::class, + 'type_options' => [ + 'url' => function (Product $product): string { + return $this->urlGenerator->generate('category_show', [ + 'id' => $product->getId(), + ]); + }), + ], + 'visible' => function (Product $product): bool { + return $product->isActive(); + } + ], + ], + ]) +; +``` + +## Inherited options + + diff --git a/docs/src/reference/types/column/boolean.md b/docs/src/reference/types/column/boolean.md new file mode 100644 index 00000000..40e29aa8 --- /dev/null +++ b/docs/src/reference/types/column/boolean.md @@ -0,0 +1,27 @@ + + +# BooleanColumnType + +The [`BooleanColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/NumberColumnType.php) represents a column with value displayed as a "yes" or "no". + +## Options + +### `label_true` + +- **type**: `string` or `Symfony\Component\Translation\TranslatableInterface` +- **default**: `'Yes'` + +Sets the value that will be displayed if value is truthy. + +### `label_false` + +- **type**: `string` or `Symfony\Component\Translation\TranslatableInterface` +- **default**: `'No'` + +Sets the value that will be displayed if row value is falsy. + +## Inherited options + + diff --git a/docs/src/reference/types/column/checkbox.md b/docs/src/reference/types/column/checkbox.md new file mode 100644 index 00000000..46dff6c6 --- /dev/null +++ b/docs/src/reference/types/column/checkbox.md @@ -0,0 +1,32 @@ + + +# CheckboxColumnType + +The [`CheckboxColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/CheckboxColumnType.php) represents a column with checkboxes, both in header and value rows. + +::: info In most cases, it is not necessary to use this column type directly. +Instead, use data table builder's `addBatchAction()` method. +If at least one batch action is defined and is visible, an `BatchActionType` is added to the data table. + +For details, see [adding checkbox column](../../../docs/components/actions.md#adding-checkbox-column) section of the action documentation. +::: + +## Options + +### `identifier_name` + +- **type**: `string` +- **default**: `'id'` + +A name of the property to use in the batch actions. + +For more details about this option's influence on the batch actions, see ["Changing the identifier parameter name"](#) section. + +## Inherited options + + diff --git a/docs/reference/columns/types/collection.md b/docs/src/reference/types/column/collection.md old mode 100755 new mode 100644 similarity index 60% rename from docs/reference/columns/types/collection.md rename to docs/src/reference/types/column/collection.md index eb82aa69..490f5abb --- a/docs/reference/columns/types/collection.md +++ b/docs/src/reference/types/column/collection.md @@ -1,19 +1,10 @@ ---- -label: Collection -order: g -tags: - - columns ---- + -# Collection column type +# CollectionColumnType -The `CollectionColumnType` represents a column with value displayed as a list. - -+-------------+---------------------------------------------------------------------+ -| Parent type | [ColumnType](column) -+-------------+---------------------------------------------------------------------+ -| Class | [:icon-mark-github: CollectionColumnType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/CollectionColumnType.php) -+-------------+---------------------------------------------------------------------+ +The [`CollectionColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/CollectionColumnType.php) represents a column with value displayed as a list. ## Options @@ -22,8 +13,8 @@ The `CollectionColumnType` represents a column with value displayed as a list. - **type**: `string` - **default**: `'Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType'` -This is the column type for each item in this collection (e.g. [TextColumnType](text), [LinkColumnType](link), etc). -For example, if you have an array of entities, you'd probably want to use the [LinkColumnType](link) to display them as links to their details view. +This is the column type for each item in this collection (e.g. [TextColumnType](text.md), [LinkColumnType](link.md), etc). +For example, if you have an array of entities, you'd probably want to use the [LinkColumnType](link.md) to display them as links to their details view. ### `entry_options` @@ -31,10 +22,10 @@ For example, if you have an array of entities, you'd probably want to use the [L - **default**: `['property_path' => false]` This is the array that's passed to the column type specified in the entry_type option. -For example, if you used the [LinkColumnType](link) as your `entry_type` option (e.g. for a collection of links of product tags), +For example, if you used the [LinkColumnType](link.md) as your `entry_type` option (e.g. for a collection of links of product tags), then you'd want to pass the href option to the underlying type: -```php # +```php use Kreyu\Bundle\DataTableBundle\Column\Type\CollectionColumnType; use Kreyu\Bundle\DataTableBundle\Column\Type\LinkColumnType; @@ -68,4 +59,4 @@ Sets the value displayed between every item in the collection. ## Inherited options -{{ include '_column_options' }} + diff --git a/docs/src/reference/types/column/column.md b/docs/src/reference/types/column/column.md new file mode 100644 index 00000000..2992eedc --- /dev/null +++ b/docs/src/reference/types/column/column.md @@ -0,0 +1,11 @@ + + +# ColumnType + +The [`ColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/ColumnType.php) represents a base column, used as a parent for every other column type in the bundle. + +## Options + + diff --git a/docs/src/reference/types/column/date-period.md b/docs/src/reference/types/column/date-period.md new file mode 100644 index 00000000..646e1c97 --- /dev/null +++ b/docs/src/reference/types/column/date-period.md @@ -0,0 +1,34 @@ + + +# DatePeriodColumnType + +The [`DatePeriodColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/DatePeriodColumnType.php) represents a column with value displayed as a date (and with time by default). + +## Options + +### `format` + +- **type**: `string` +- **default**: `'d.m.Y H:i:s'` + +The format specifier is the same as supported by [date](https://www.php.net/date). + +### `timezone` + +- **type**: `null` or `string` +- **default**: `null` + +A timezone used to render the dates as string. + +### `separator` + +- **type**: `string` +- **default**: `' - '` + +Separator to display between the dates. + +## Inherited options + + diff --git a/docs/src/reference/types/column/date-time.md b/docs/src/reference/types/column/date-time.md new file mode 100644 index 00000000..70439492 --- /dev/null +++ b/docs/src/reference/types/column/date-time.md @@ -0,0 +1,27 @@ + + +# DateTimeColumnType + +The [`DateTimeColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/DateTimeColumnType.php) represents a column with value displayed as a date and time. + +## Options + +### `format` + +- **type**: `string` +- **default**: `'d.m.Y H:i:s'` + +The format specifier is the same as supported by [date](https://www.php.net/date). + +### `timezone` + +- **type**: `null` or `string` +- **default**: `null` + +A timezone used to render the date time as string. + +## Inherited options + + diff --git a/docs/src/reference/types/column/date.md b/docs/src/reference/types/column/date.md new file mode 100644 index 00000000..25257a9c --- /dev/null +++ b/docs/src/reference/types/column/date.md @@ -0,0 +1,29 @@ + + +# DateColumnType + +The [`DateColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/DateColumnType.php) represents a column with value displayed as a date. + +This column type works exactly like `DateTimeColumnType`, but has a different default format. + +## Options + +### `format` + +- **type**: `string` +- **default**: `'d.m.Y'` + +The format specifier is the same as supported by [date](https://www.php.net/date). + +### `timezone` + +- **type**: `null` or `string` +- **default**: `null` + +A timezone used to render the date time as string. + +## Inherited options + + diff --git a/docs/reference/columns/types/link.md b/docs/src/reference/types/column/link.md old mode 100755 new mode 100644 similarity index 56% rename from docs/reference/columns/types/link.md rename to docs/src/reference/types/column/link.md index 9854a523..6462e2ea --- a/docs/reference/columns/types/link.md +++ b/docs/src/reference/types/column/link.md @@ -1,19 +1,10 @@ ---- -label: Link -order: d -tags: - - columns ---- + -# Link column type +# LinkColumnType -The `LinkColumnType` represents a column with value displayed as a link. - -+-------------+---------------------------------------------------------------------+ -| Parent type | [ColumnType](column) -+-------------+---------------------------------------------------------------------+ -| Class | [:icon-mark-github: LinkColumnType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/LinkColumnType.php) -+-------------+---------------------------------------------------------------------+ +The [`LinkColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/LinkColumnType.php) represents a column with value displayed as a link. ## Options @@ -24,7 +15,7 @@ The `LinkColumnType` represents a column with value displayed as a link. Sets the value that will be used as a [href attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-href). -```php # +```php use App\Entity\Category; use Kreyu\Bundle\DataTableBundle\Column\Type\LinkColumnType; @@ -48,4 +39,4 @@ Sets the value that will be used as a [target attribute](https://developer.mozil ## Inherited options -{{ include '_column_options' }} + diff --git a/docs/reference/columns/types/money.md b/docs/src/reference/types/column/money.md old mode 100755 new mode 100644 similarity index 79% rename from docs/reference/columns/types/money.md rename to docs/src/reference/types/column/money.md index 8f06de13..3c254c23 --- a/docs/reference/columns/types/money.md +++ b/docs/src/reference/types/column/money.md @@ -1,19 +1,10 @@ ---- -label: Money -order: c -tags: - - columns ---- + -# Money column type +# MoneyColumnType -The `MoneyColumnType` represents a column with monetary value, appropriately formatted and rendered with currency sign. - -+-------------+---------------------------------------------------------------------+ -| Parent type | [NumberType](number.md) -+-------------+---------------------------------------------------------------------+ -| Class | [:icon-mark-github: MoneyColumnType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/MoneyColumnType.php) -+-------------+---------------------------------------------------------------------+ +The [`MoneyColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/MoneyColumnType.php) represents a column with monetary value, appropriately formatted and rendered with currency sign. ## Options @@ -87,4 +78,4 @@ For more details, see: ## Inherited options -{{ include '_column_options' }} + diff --git a/docs/reference/columns/types/number.md b/docs/src/reference/types/column/number.md old mode 100755 new mode 100644 similarity index 66% rename from docs/reference/columns/types/number.md rename to docs/src/reference/types/column/number.md index 262dec28..5c50a04e --- a/docs/reference/columns/types/number.md +++ b/docs/src/reference/types/column/number.md @@ -1,19 +1,10 @@ ---- -label: Number -order: b -tags: - - columns ---- + -# Number column type +# NumberColumnType -The `NumberColumnType` represents a column with value displayed as a number. - -+-------------+---------------------------------------------------------------------+ -| Parent type | [ColumnType](column) -+-------------+---------------------------------------------------------------------+ -| Class | [:icon-mark-github: NumberColumnType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/NumberColumnType.php) -+-------------+---------------------------------------------------------------------+ +The [`NumberColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/NumberColumnType.php) represents a column with value displayed as a number. ## Options @@ -40,7 +31,7 @@ For example, to limit decimal places to two: ```php $builder - ->addColumn('price', MoneyColumnType::class, [ + ->addColumn('price', NumberColumnType::class, [ 'intl_formatter_options' => [ 'attrs' => [ // https://www.php.net/manual/en/class.numberformatter.php#numberformatter.constants.max-fraction-digits @@ -57,4 +48,4 @@ For more details, see: ## Inherited options -{{ include '_column_options' }} + diff --git a/docs/reference/columns/types/_column_options.md b/docs/src/reference/types/column/options/column.md old mode 100755 new mode 100644 similarity index 81% rename from docs/reference/columns/types/_column_options.md rename to docs/src/reference/types/column/options/column.md index e99a81d2..a067cbb6 --- a/docs/reference/columns/types/_column_options.md +++ b/docs/src/reference/types/column/options/column.md @@ -1,9 +1,28 @@ + + ### `label` - **type**: `null`, `string` or `Symfony\Component\Translation\TranslatableInterface` -- **default**: {{ option_label_default_value ?? '`null` - the label is "guessed" from the column name' }} +- **default**: `{{ defaults.label }}` + +Sets the label that will be used in column header and personalization column list. -Sets the label that will be used when rendering the column header. +When value equals `null`, a sentence cased column name is used as a label, for example: + +| Column name | Guessed label | +|-------------|---------------| +| name | Name | +| firstName | First name | ### `header_translation_domain` @@ -31,12 +50,12 @@ Setting the option to `false` disables its translation. ### `property_path` - **type**: `null`, `false` or `string` -- **default**: {{ option_property_path_default_value ?? '`null` - the property path is "guessed" from the column name' }} +- **default**: `{{ defaults.property_path }}` Sets the property path used by the [PropertyAccessor](https://symfony.com/doc/current/components/property_access.html) to retrieve column value of each row. Setting the option to `false` disables property accessor. -```php # +```php $builder ->addColumn('category', TextColumnType::class, [ 'property_path' => 'category.name', @@ -44,6 +63,8 @@ $builder ; ``` +When value equals `null`, the column name is used as a property path. + ### `getter` - **type**: `null` or `callable` @@ -52,7 +73,7 @@ $builder When provided, this callable will be invoked to read the value from the underlying object that will be used within the column. This disables the usage of the [PropertyAccessor](https://symfony.com/doc/current/components/property_access.html), described in the [property_path](#property_path) option. -```php # +```php $builder ->addColumn('category', TextColumnType::class, [ 'getter' => fn (Product $product) => $product->getCategory(), @@ -65,18 +86,18 @@ $builder - **type**: `bool` or `string` - **default**: `false` - the sortable behavior is disabled -Sets the sort field used by the sortable behavior. - -Setting the option to `true` enables column sorting and uses the column name as a sort field name. -Setting the option to `false` disables column sorting. +Sets the sort field used by the sortable behavior. Setting the option to: +- `true` - enables column sorting and uses the column name as a sort field name; +- `false` - disables column sorting; +- string - defines sort property path; ### `block_prefix` - **type**: `string` -- **default**: column type block prefix +- **default**: value returned by the column type `getBlockPrefix()` method Allows you to add a custom block prefix and override the block name used to render the column type. -Useful for example if you have multiple instances of the same column type, and you need to personalize +Useful for example if you have multiple instances of the same column type, and you need to personalize the rendering of some of them, without the need to create a new column type. ### `formatter` @@ -86,7 +107,7 @@ the rendering of some of them, without the need to create a new column type. Formats the value to the desired string. -```php # +```php $builder ->addColumn('quantity', NumberColumnType::class, [ 'formatter' => fn (float $value) => number_format($value, 2) . 'kg', @@ -104,7 +125,7 @@ It is used to differentiate options for regular rendering, and excel rendering. For example, if you wish to display quantity column with "Quantity" label, but export with a "Qty" header: -```php # +```php $builder ->addColumn('quantity', NumberColumnType::class, [ 'label' => 'Quantity', @@ -131,7 +152,7 @@ If you want to add extra attributes to an HTML column header representation (`addColumn('quantity', NumberColumnType::class, [ 'header_attr' => [ @@ -150,7 +171,7 @@ If you want to add extra attributes to an HTML column value representation (`addColumn('quantity', NumberColumnType::class, [ 'value_attr' => [ @@ -162,7 +183,7 @@ $builder You can pass a `callable` to perform a dynamic attribute generation: -```php # +```php $builder ->addColumn('quantity', NumberColumnType::class, [ 'value_attr' => function (int $quantity, Product $product) { @@ -185,7 +206,7 @@ This option changes the column rendering priority, allowing you to display colum 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). +**Note**: column priority can be changed by the [personalization feature](../../../../docs/features/personalization.md). ### `visible` @@ -194,7 +215,7 @@ Priority can albo be negative and columns with the same priority will keep their Determines whether the column is visible to the user. -**Note**: column visibility can be changed by the [personalization feature](../../../features/personalization.md). +**Note**: column visibility can be changed by the [personalization feature](../../../../docs/features/personalization.md). ### `personalizable` @@ -202,4 +223,4 @@ Determines whether the column is visible to the user. - **default**: `true` Determines whether the column is personalizable. -The non-personalizable columns are not modifiable by the [personalization feature](../../../features/personalization.md). +The non-personalizable columns are not modifiable by the [personalization feature](../../../../docs/features/personalization.md). diff --git a/docs/src/reference/types/column/template.md b/docs/src/reference/types/column/template.md new file mode 100644 index 00000000..82c8b705 --- /dev/null +++ b/docs/src/reference/types/column/template.md @@ -0,0 +1,25 @@ + + +# TemplateColumnType + +The [`TemplateColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/TemplateColumnType.php) represents a column with value displayed as a Twig template. + +## Options + +### `template_path` + +- **type**: `string` or `callable` + +Sets the path to the template that should be rendered. + +### `template_vars` + +- **type**: `string` or `callable` + +Sets the variables used within the template. + +## Inherited options + + diff --git a/docs/src/reference/types/column/text.md b/docs/src/reference/types/column/text.md new file mode 100644 index 00000000..195a21ce --- /dev/null +++ b/docs/src/reference/types/column/text.md @@ -0,0 +1,15 @@ + + +# TextColumnType + +The [`TextColumnType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/TextColumnType.php) represents a column with value displayed as a text. + +## Options + +This column type has no additional options. + +## Inherited options + + diff --git a/docs/src/reference/types/data-table.md b/docs/src/reference/types/data-table.md new file mode 100644 index 00000000..82da5634 --- /dev/null +++ b/docs/src/reference/types/data-table.md @@ -0,0 +1,150 @@ +# DataTable type + +The [`DataTableType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/ColumnType.php) represents a base data table, and should be used as a base for every data table defined in the system. + +## Options + +### `title` + +- **type**: `null`, `string` or `TranslatableInterface` +- **default**: `null` + +### `title_translation_parameters` + +- **type**: `array` +- **default**: `[]` + +### `translation_domain` + +- **type**: `null`, `bool` or `string` +- **default**: `null` + +### `themes` + +- **type**: `string[]` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `column_factory` + +- **type**: `ColumnFactoryInterface` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `filter_factory` + +- **type**: `FilterFactoryInterface` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `action_factory` + +- **type**: `ActionFactoryInterface` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `exporter_factory` + +- **type**: `ExporterFactoryInterface` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `request_handler` + +- **type**: `RequestHandlerInterface` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `sorting_enabled` + +- **type**: `bool` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `sorting_persistence_enabled` + +- **type**: `bool` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `sorting_persistence_adapter` + +- **type**: `null` or `PersistenceAdapterInterface` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `sorting_persistence_subject_provider` + +- **type**: `null` or `PersistenceSubjectProviderInterface` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `pagination_enabled` + +- **type**: `bool` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `pagination_persistence_enabled` + +- **type**: `bool` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `pagination_persistence_adapter` + +- **type**: `null` or `PersistenceAdapterInterface` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `pagination_persistence_subject_provider` + +- **type**: `null` or `PersistenceSubjectProviderInterface` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `filtration_enabled` + +- **type**: `bool` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `filtration_persistence_enabled` + +- **type**: `bool` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `filtration_persistence_adapter` + +- **type**: `null` or `PersistenceAdapterInterface` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `filtration_persistence_subject_provider` + +- **type**: `null` or `PersistenceSubjectProviderInterface` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `filtration_form_factory` + +- **type**: `null` or `FormFactoryInterface` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `personalization_enabled` + +- **type**: `bool` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `personalization_persistence_enabled` + +- **type**: `bool` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `personalization_persistence_adapter` + +- **type**: `null` or `PersistenceAdapterInterface` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `personalization_persistence_subject_provider` + +- **type**: `null` or `PersistenceSubjectProviderInterface` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `personalization_form_factory` + +- **type**: `null` or `FormFactoryInterface` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `exporting_enabled` + +- **type**: `bool` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) + +### `exporting_form_factory` + +- **type**: `null` or `FormFactoryInterface` +- **default**: value defined in [`defaults` configuration](../configuration.md#data-table-builder-defaults) diff --git a/docs/src/reference/types/exporter.md b/docs/src/reference/types/exporter.md new file mode 100644 index 00000000..accff261 --- /dev/null +++ b/docs/src/reference/types/exporter.md @@ -0,0 +1,25 @@ +# Exporter types + +The following action types are natively available in the bundle: + +- [Callback](#) +- [Exporter](#) + +## PhpSpreadsheet + +The following [PhpSpreadsheet](https://github.com/PHPOffice/PhpSpreadsheet) exporter types are natively available in the bundle: + +- [Csv](./exporter/php-spreadsheet/csv) +- [Xls](./exporter/php-spreadsheet/xls) +- [Xlsx](./exporter/php-spreadsheet/xlsx) +- [Ods](./exporter/php-spreadsheet/ods) +- [Pdf](./exporter/php-spreadsheet/pdf) +- [Html](./exporter/php-spreadsheet/html) + +## OpenSpout + +The following [OpenSpout](https://github.com/openspout/openspout) exporter types are natively available in the bundle: + +- [Csv](./exporter/open-spout/csv) +- [Xlsx](./exporter/open-spout/xlsx) +- [Ods](./exporter/open-spout/ods) diff --git a/docs/src/reference/types/exporter/callback.md b/docs/src/reference/types/exporter/callback.md new file mode 100644 index 00000000..3f7e3f7d --- /dev/null +++ b/docs/src/reference/types/exporter/callback.md @@ -0,0 +1,34 @@ + + +# CallbackExporterType + +The [`CallbackExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/CallbackExporterType.php) represents a filter that uses a given callback as its handler. + +## Options + +### `callback` + +- **type**: `callable` + +Sets callable that works as an exporter handler. + +```php +use Kreyu\Bundle\DataTableBundle\Exporter\Type\CallbackExporterType; +use Kreyu\Bundle\DataTableBundle\Exporter\ExporterInterface; +use Kreyu\Bundle\DataTableBundle\Exporter\ExportFile; +use Kreyu\Bundle\DataTableBundle\DataTableView; + +$builder + ->addExporter('txt', CallbackExporterType::class, [ + 'callback' => function (DataTableView $view, ExporterInterface $exporter, string $filename): ExportFile { + // ... + }, + ]) +; +``` + +## Inherited options + + diff --git a/docs/src/reference/types/exporter/exporter.md b/docs/src/reference/types/exporter/exporter.md new file mode 100644 index 00000000..b5c0515b --- /dev/null +++ b/docs/src/reference/types/exporter/exporter.md @@ -0,0 +1,11 @@ + + +# ExporterType + +The [`ExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Exporter/Type/CallbackExporterType.php) represents a base exporter used as a parent for every other exporter type in the bundle. + +## Options + + diff --git a/docs/src/reference/types/exporter/open-spout/csv.md b/docs/src/reference/types/exporter/open-spout/csv.md new file mode 100644 index 00000000..cff62d1f --- /dev/null +++ b/docs/src/reference/types/exporter/open-spout/csv.md @@ -0,0 +1,41 @@ + + +# OpenSpout CsvExporterType + +The [`CsvExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/OpenSpout/Exporter/Type/CsvExporterType.php) represents an exporter that uses an [OpenSpout](https://github.com/openspout/openspout) CSV writer. + +## Options + +### `field_delimiter` + +- **type**: `string` +- **default**: `','` + +Represents a string that separates the values. + +### `field_enclosure` + +- **type**: `string` +- **default**: `'"'` + +Represents a string that wraps the values. + +### `should_add_bom` + +- **type**: `bool` +- **default**: `true` + +Determines whether a BOM character should be added at the beginning of the file. + +### `flush_threshold` + +- **type**: `int` +- **default**: `500` + +Represents a number of rows after which the output should be flushed to a file. + +## Inherited options + + \ No newline at end of file diff --git a/docs/src/reference/types/exporter/open-spout/ods.md b/docs/src/reference/types/exporter/open-spout/ods.md new file mode 100644 index 00000000..b1c458b1 --- /dev/null +++ b/docs/src/reference/types/exporter/open-spout/ods.md @@ -0,0 +1,42 @@ + + +# OpenSpout OdsExporterType + +The [`OdsExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/OpenSpout/Exporter/Type/OdsExporterType.php) represents an exporter that uses an [OpenSpout](https://github.com/openspout/openspout) ODS writer. + +## Options + +### `default_row_style` + +- **type**: `OpenSpout\Common\Entity\Style\Style` +- **default**: an unmodified instance of `Style` class + +An instance of style class that will be applied to all rows. + +### `should_create_new_sheets_automatically` + +- **type**: `bool` +- **default**: `true` + +Determines whether new sheets should be created automatically +when the maximum number of rows (1,048,576) per sheet is reached. + +### `default_column_width` + +- **type**: `null` or `float` +- **default**: `null` + +Represents a width that will be applied to all columns by default. + +### `default_row_height` + +- **type**: `null` or `float` +- **default**: `null` + +Represents a height that will be applied to all rows by default. + +## Inherited options + + \ No newline at end of file diff --git a/docs/reference/exporters/types/open-spout/xlsx.md b/docs/src/reference/types/exporter/open-spout/xlsx.md old mode 100755 new mode 100644 similarity index 57% rename from docs/reference/exporters/types/open-spout/xlsx.md rename to docs/src/reference/types/exporter/open-spout/xlsx.md index c6037421..8c16b624 --- a/docs/reference/exporters/types/open-spout/xlsx.md +++ b/docs/src/reference/types/exporter/open-spout/xlsx.md @@ -1,20 +1,10 @@ ---- -label: XLSX -order: b -tags: - - exporters - - openspout ---- + -# OpenSpout XLSX exporter type +# OpenSpout XlsxExporterType -The `XlsxExporterType` represents an exporter that uses an [OpenSpout](https://github.com/openspout/openspout) XLSX writer. - -+---------------------+--------------------------------------------------------------+ -| 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) -+---------------------+--------------------------------------------------------------+ +The [`XlsxExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/OpenSpout/Exporter/Type/XlsxExporterType.php) represents an exporter that uses an [OpenSpout](https://github.com/openspout/openspout) XLSX writer. ## Options @@ -58,5 +48,4 @@ Represents a height that will be applied to all rows by default. ## Inherited options -{{ include '_open-spout_options.md' }} -{{ include '../_exporter_options.md' }} + \ No newline at end of file diff --git a/docs/reference/exporters/types/_exporter_options.md b/docs/src/reference/types/exporter/options/exporter.md old mode 100755 new mode 100644 similarity index 88% rename from docs/reference/exporters/types/_exporter_options.md rename to docs/src/reference/types/exporter/options/exporter.md index 6ea654ca..b9d2eb67 --- a/docs/reference/exporters/types/_exporter_options.md +++ b/docs/src/reference/types/exporter/options/exporter.md @@ -1,13 +1,13 @@ ### `use_headers` -- **type**: `bool` +- **type**: `bool` - **default**: `true` Determines whether the exporter should add headers to the output file. ### `label` -- **type**: `null` or `string` +- **type**: `null` or `string` - **default**: `null` the label is "guessed" from the exporter name Sets the label of the exporter, visible in the export action modal. @@ -21,7 +21,7 @@ Sets the directory used to store temporary file during the export process. ### `tempnam_prefix` -- **type**: `string` +- **type**: `string` - **default**: `exporter_` Sets the prefix used to generate temporary file names during the export process. \ No newline at end of file diff --git a/docs/reference/exporters/types/php-spreadsheet/_php-spreadsheet_options.md b/docs/src/reference/types/exporter/options/php-spreadsheet.md old mode 100755 new mode 100644 similarity index 83% rename from docs/reference/exporters/types/php-spreadsheet/_php-spreadsheet_options.md rename to docs/src/reference/types/exporter/options/php-spreadsheet.md index 0f0d1597..f59ae356 --- a/docs/reference/exporters/types/php-spreadsheet/_php-spreadsheet_options.md +++ b/docs/src/reference/types/exporter/options/php-spreadsheet.md @@ -1,10 +1,8 @@ ### `pre_calculate_formulas` -- **type**: `bool` +- **type**: `bool` - **default**: `true` By default, the PhpSpreadsheet writers pre-calculates all formulas in the spreadsheet. This can be slow on large spreadsheets, and maybe even unwanted. The value of this option determines whether the formula pre-calculation is enabled. - -{{ include '../_exporter_options' }} \ No newline at end of file diff --git a/docs/reference/exporters/types/php-spreadsheet/csv.md b/docs/src/reference/types/exporter/php-spreadsheet/csv.md old mode 100755 new mode 100644 similarity index 77% rename from docs/reference/exporters/types/php-spreadsheet/csv.md rename to docs/src/reference/types/exporter/php-spreadsheet/csv.md index 1fed945f..c7ee8f46 --- a/docs/reference/exporters/types/php-spreadsheet/csv.md +++ b/docs/src/reference/types/exporter/php-spreadsheet/csv.md @@ -1,17 +1,11 @@ ---- -label: CSV -order: a ---- + -# PhpSpreadsheet CSV exporter type +# PhpSpreadsheet CsvExporterType -The `CsvExporterType` represents an exporter that uses a [PhpSpreadsheet CSV writer](https://phpspreadsheet.readthedocs.io/en/latest/topics/reading-and-writing-to-file/#phpofficephpspreadsheetwritercsv). - -+---------------------+--------------------------------------------------------------+ -| Parent type | [PhpSpreadsheetType](php-spreadsheet.md) -+---------------------+--------------------------------------------------------------+ -| Class | [:icon-mark-github: CsvExporterType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/CsvExporterType.php) -+---------------------+--------------------------------------------------------------+ +The [`CsvExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/CsvExporterType.php) represents an exporter that uses an [PhpSpreadsheet](https://github.com/PHPOffice/PhpSpreadsheet) CSV writer. ## Options @@ -92,4 +86,9 @@ If the worksheet you are exporting contains numbers with thousands separators, then you should think about what characters you want to use for those before doing the export. By default, PhpSpreadsheet looks up in the server's locale settings to decide what character to use. -But to avoid problems it is recommended to set the character explicitly. \ No newline at end of file +But to avoid problems it is recommended to set the character explicitly. + +## Inherited options + + + \ No newline at end of file diff --git a/docs/reference/exporters/types/php-spreadsheet/_html_options.md b/docs/src/reference/types/exporter/php-spreadsheet/html.md old mode 100755 new mode 100644 similarity index 77% rename from docs/reference/exporters/types/php-spreadsheet/_html_options.md rename to docs/src/reference/types/exporter/php-spreadsheet/html.md index 691374a7..6f4dfd01 --- a/docs/reference/exporters/types/php-spreadsheet/_html_options.md +++ b/docs/src/reference/types/exporter/php-spreadsheet/html.md @@ -1,6 +1,17 @@ + + +# PhpSpreadsheet HtmlExporterType + +The [`HtmlExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/HtmlExporterType.php) represents an exporter that uses an [PhpSpreadsheet](https://github.com/PHPOffice/PhpSpreadsheet) HTML writer. + +## Options + ### `sheet_index` -- **type**: `null` or `int` +- **type**: `null` or `int` - **default**: `0` HTML files can only contain one or more worksheets. @@ -9,7 +20,7 @@ If you want to write all sheets into a single HTML file, set this option to `nul ### `images_root` -- **type**: `string` +- **type**: `string` - **default**: `''` There might be situations where you want to explicitly set the included images root. For example, instead of: @@ -38,28 +49,28 @@ $builder ### `embed_images` -- **type**: `bool` +- **type**: `bool` - **default**: `false` Determines whether the images should be embedded or not. ### `use_inline_css` -- **type**: `bool` +- **type**: `bool` - **default**: `false` Determines whether the inline css should be used or not. ### `generate_sheet_navigation_block` -- **type**: `bool` +- **type**: `bool` - **default**: `true` Determines whether the sheet navigation block should be generated or not. ### `edit_html_callback` -- **type**: `null` or `callable` +- **type**: `null` or `callable` - **default**: `null` Accepts a callback function to edit the generated html before saving. @@ -83,7 +94,7 @@ $builder ### `decimal_separator` -- **type**: `string` +- **type**: `string` - **default**: depends on the server's locale setting If the worksheet you are exporting contains numbers with decimal separators, @@ -94,11 +105,16 @@ But to avoid problems, it is recommended to set the character explicitly. ### `thousands_separator` -- **type**: `string` +- **type**: `string` - **default**: depends on the server's locale setting If the worksheet you are exporting contains numbers with thousands separators, then you should think about what characters you want to use for those before doing the export. By default, PhpSpreadsheet looks up in the server's locale settings to decide what character to use. -But to avoid problems, it is recommended to set the character explicitly. \ No newline at end of file +But to avoid problems, it is recommended to set the character explicitly. + +## Inherited options + + + diff --git a/docs/src/reference/types/exporter/php-spreadsheet/ods.md b/docs/src/reference/types/exporter/php-spreadsheet/ods.md new file mode 100644 index 00000000..bf069efb --- /dev/null +++ b/docs/src/reference/types/exporter/php-spreadsheet/ods.md @@ -0,0 +1,17 @@ + + +# PhpSpreadsheet OdsExporterType + +The [`XlsxExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/XlsxExporterType.php) represents an exporter that uses an [PhpSpreadsheet](https://github.com/PHPOffice/PhpSpreadsheet) ODS writer. + +## Options + +This exporter type has no additional options. + +## Inherited options + + + diff --git a/docs/src/reference/types/exporter/php-spreadsheet/pdf.md b/docs/src/reference/types/exporter/php-spreadsheet/pdf.md new file mode 100644 index 00000000..cba7fe7b --- /dev/null +++ b/docs/src/reference/types/exporter/php-spreadsheet/pdf.md @@ -0,0 +1,120 @@ + + +# PhpSpreadsheet PdfExporterType + +The [`PdfExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/PdfExporterType.php) represents an exporter that uses an [PhpSpreadsheet](https://github.com/PHPOffice/PhpSpreadsheet) PDF writer. + +## Options + +### `sheet_index` + +- **type**: `null` or `int` +- **default**: `0` + +PDF files can only contain one or more worksheets. +Therefore, you can specify which sheet to write to PDF. +If you want to write all sheets into a single PDF file, set this option to `null`. + +### `images_root` + +- **type**: `string` +- **default**: `''` + +There might be situations where you want to explicitly set the included images root. For example, instead of: + +```html + +``` + +You might want to see: + +```html + +``` + +Use this option to achieve this result: + +```php +use Kreyu\Bundle\DataTableBundle\Bridge\PhpSpreadsheet\Exporter\Type\PdfExporterType; + +$builder + ->addExporter('html', PdfExporterType::class, [ + 'images_root' => 'https://www.domain.com', + ]) +; +``` + +### `embed_images` + +- **type**: `bool` +- **default**: `false` + +Determines whether the images should be embedded or not. + +### `use_inline_css` + +- **type**: `bool` +- **default**: `false` + +Determines whether the inline css should be used or not. + +### `generate_sheet_navigation_block` + +- **type**: `bool` +- **default**: `true` + +Determines whether the sheet navigation block should be generated or not. + +### `edit_html_callback` + +- **type**: `null` or `callable` +- **default**: `null` + +Accepts a callback function to edit the generated html before saving. +For example, you could change the gridlines from a thin solid black line: + +```php +use Kreyu\Bundle\DataTableBundle\Bridge\PhpSpreadsheet\Exporter\Type\PdfExporterType; + +$builder + ->addExporter('html', PdfExporterType::class, [ + 'edit_html_callback' => function (string $html): string { + return str_replace( + '{border: 1px solid black;}', + '{border: 2px dashed red;}', + $html, + ); + } + ]) +; +``` + +### `decimal_separator` + +- **type**: `string` +- **default**: depends on the server's locale setting + +If the worksheet you are exporting contains numbers with decimal separators, +then you should think about what characters you want to use for those before doing the export. + +By default, PhpSpreadsheet looks up in the server's locale settings to decide what character to use. +But to avoid problems, it is recommended to set the character explicitly. + +### `thousands_separator` + +- **type**: `string` +- **default**: depends on the server's locale setting + +If the worksheet you are exporting contains numbers with thousands separators, +then you should think about what characters you want to use for those before doing the export. + +By default, PhpSpreadsheet looks up in the server's locale settings to decide what character to use. +But to avoid problems, it is recommended to set the character explicitly. + +## Inherited options + + + diff --git a/docs/src/reference/types/exporter/php-spreadsheet/xls.md b/docs/src/reference/types/exporter/php-spreadsheet/xls.md new file mode 100644 index 00000000..fd908f7d --- /dev/null +++ b/docs/src/reference/types/exporter/php-spreadsheet/xls.md @@ -0,0 +1,17 @@ + + +# PhpSpreadsheet XlsExporterType + +The [`XlsxExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/XlsxExporterType.php) represents an exporter that uses an [PhpSpreadsheet](https://github.com/PHPOffice/PhpSpreadsheet) XLS writer. + +## Options + +This exporter type has no additional options. + +## Inherited options + + + diff --git a/docs/src/reference/types/exporter/php-spreadsheet/xlsx.md b/docs/src/reference/types/exporter/php-spreadsheet/xlsx.md new file mode 100644 index 00000000..e2373e68 --- /dev/null +++ b/docs/src/reference/types/exporter/php-spreadsheet/xlsx.md @@ -0,0 +1,23 @@ + + +# PhpSpreadsheet XlsxExporterType + +The [`XlsxExporterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/PhpSpreadsheet/Exporter/Type/XlsxExporterType.php) represents an exporter that uses an [PhpSpreadsheet](https://github.com/PHPOffice/PhpSpreadsheet) XLSX writer. + +## Options + +### `office_2003_compatibility` + +- **type**: `bool` +- **default**: `false` + +Because of a bug in the Office2003 compatibility pack, there can be some small issues when opening +Xlsx spreadsheets (mostly related to formula calculation). You can enable Office2003 compatibility by setting this option to `true`. + +## Inherited options + + + diff --git a/docs/src/reference/types/filter.md b/docs/src/reference/types/filter.md new file mode 100644 index 00000000..99d13f0d --- /dev/null +++ b/docs/src/reference/types/filter.md @@ -0,0 +1,20 @@ +# Filter types + +The following filter types are natively available in the bundle: + +- [Callback](types/callback.md) +- [Search](types/search.md) +- [Filter](types/filter.md) + +## Doctrine ORM + +The built-in Doctrine ORM integration provides additional filter types: + +- [String](filter/doctrine-orm/string.md) +- [Numeric](filter/doctrine-orm/numeric.md) +- [Boolean](filter/doctrine-orm/boolean.md) +- [Date](filter/doctrine-orm/date.md) +- [DateTime](filter/doctrine-orm/date-time.md) +- [DateRange](filter/doctrine-orm/date-range.md) +- [Entity](filter/doctrine-orm/entity.md) +- [DoctrineOrm](filter/doctrine-orm/doctrine-orm.md) diff --git a/docs/src/reference/types/filter/callback.md b/docs/src/reference/types/filter/callback.md new file mode 100644 index 00000000..22502970 --- /dev/null +++ b/docs/src/reference/types/filter/callback.md @@ -0,0 +1,34 @@ + + +# CallbackFilterType + +The [`CallbackFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/CallbackFilterType.php) represents a filter that uses a given callback as its handler. + +## Options + +### `callback` + +- **type**: `callable` + +Sets callable that works as a filter handler. + +```php +use Kreyu\Bundle\DataTableBundle\Filter\Type\CallbackFilterType; +use Kreyu\Bundle\DataTableBundle\Filter\FilterData; +use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface; +use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface; + +$builder + ->addFilter('name', CallbackFilterType::class, [ + 'callback' => function (ProxyQueryInterface $query, FilterData $data, FilterInterface $filter): void { + // ... + }, + ]) +; +``` + +## Inherited options + + diff --git a/docs/src/reference/types/filter/doctrine-orm/boolean.md b/docs/src/reference/types/filter/doctrine-orm/boolean.md new file mode 100644 index 00000000..d906ffd9 --- /dev/null +++ b/docs/src/reference/types/filter/doctrine-orm/boolean.md @@ -0,0 +1,21 @@ + + +# Doctrine ORM BooleanFilterType + +The Doctrine ORM [`BooleanFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/Type/BooleanFilterType.php) represents a filter that operates on a boolean values. + +## Options + +This column type has no additional options. + +## Inherited options + + + + diff --git a/docs/src/reference/types/filter/doctrine-orm/date-range.md b/docs/src/reference/types/filter/doctrine-orm/date-range.md new file mode 100644 index 00000000..431e32e9 --- /dev/null +++ b/docs/src/reference/types/filter/doctrine-orm/date-range.md @@ -0,0 +1,21 @@ + + +# Doctrine ORM DateRangeFilterType + +The Doctrine ORM [`DateRangeFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/Type/DateRangeFilterType.php) represents a filter that operates on a two date values that make a range. + +## Options + +This column type has no additional options. + +## Inherited options + + + + \ No newline at end of file diff --git a/docs/src/reference/types/filter/doctrine-orm/date-time.md b/docs/src/reference/types/filter/doctrine-orm/date-time.md new file mode 100644 index 00000000..c9ee54d3 --- /dev/null +++ b/docs/src/reference/types/filter/doctrine-orm/date-time.md @@ -0,0 +1,21 @@ + + +# Doctrine ORM DateTimeFilterType + +The Doctrine ORM [`DateTimeFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/Type/DateTimeFilterType.php) represents a filter that operates on a date time values. + +## Options + +This column type has no additional options. + +## Inherited options + + + + \ No newline at end of file diff --git a/docs/src/reference/types/filter/doctrine-orm/date.md b/docs/src/reference/types/filter/doctrine-orm/date.md new file mode 100644 index 00000000..f0dd5a89 --- /dev/null +++ b/docs/src/reference/types/filter/doctrine-orm/date.md @@ -0,0 +1,21 @@ + + +# Doctrine ORM DateFilterType + +The Doctrine ORM [`DateFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/Type/DateFilterType.php) represents a filter that operates on a date (without time) values. + +## Options + +This column type has no additional options. + +## Inherited options + + + + \ No newline at end of file diff --git a/docs/src/reference/types/filter/doctrine-orm/doctrine-orm.md b/docs/src/reference/types/filter/doctrine-orm/doctrine-orm.md new file mode 100644 index 00000000..b71e3a8d --- /dev/null +++ b/docs/src/reference/types/filter/doctrine-orm/doctrine-orm.md @@ -0,0 +1,16 @@ + + +# DoctrineOrmFilterType + +The Doctrine ORM [`DoctrineOrmFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/Type/DoctrineOrmFilterType.php) represents a base type that every other filter type from the integration uses as a parent. + +## Options + + + +## Inherited options + + diff --git a/docs/src/reference/types/filter/doctrine-orm/entity.md b/docs/src/reference/types/filter/doctrine-orm/entity.md new file mode 100644 index 00000000..a5d213da --- /dev/null +++ b/docs/src/reference/types/filter/doctrine-orm/entity.md @@ -0,0 +1,76 @@ + + +# Doctrine ORM EntityFilterType + +The Doctrine ORM [`EntityFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/Type/EntityFilterType.php) represents a filter that operates on an entity values. + +## Options + +### `choice_label` + +This is the property that should be used for displaying the entities as text in the active filter HTML element. + +```php +use App\Entity\Category; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\EntityFilterType; +// ... + +$builder->addFilter('category', EntityFilterType::class, [ + 'form_options' => [ + 'class' => Category::class, + 'choice_label' => 'displayName', // choice label for form + ], + 'choice_label' => 'displayName', // separate choice label for data table filter +]); +``` + +If left blank, the entity object will be cast to a string and so must have a `__toString()` method. You can also pass a callback function for more control: + +```php +use App\Entity\Category; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\EntityFilterType; +use Kreyu\Bundle\DataTableBundle\Filter\FilterData; +// ... + +$builder->addFilter('category', EntityFilterType::class, [ + 'form_options' => [ + 'class' => Category::class, + 'choice_label' => 'displayName', + ], + 'choice_label' => function (FilterData $data): string { + return $data->getValue()->getDisplayName(); + }, +]); +``` + +::: tip This option works like a `choice_label` option in `ChoiceType` form option. +When passing a string, the `choice_label` option is a property path. So you can use anything supported by the [PropertyAccess component](https://symfony.com/doc/current/components/property_access.html). + +For example, if the translations property is actually an associative array of objects, each with a name property, then you could do this: + +```php +use App\Entity\Category; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\EntityFilterType; +// ... + +$builder->addFilter('category', EntityFilterType::class, [ + 'form_options' => [ + 'class' => Category::class, + 'choice_label' => 'translations[en].name', + ], + 'choice_label' => 'translations[en].name', +]); +``` +::: + +## Inherited options + + + + \ No newline at end of file diff --git a/docs/src/reference/types/filter/doctrine-orm/numeric.md b/docs/src/reference/types/filter/doctrine-orm/numeric.md new file mode 100644 index 00000000..760fdf32 --- /dev/null +++ b/docs/src/reference/types/filter/doctrine-orm/numeric.md @@ -0,0 +1,20 @@ + + +# Doctrine ORM NumericFilterType + +The Doctrine ORM [`NumericFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/Type/NumericFilterType.php) represents a filter that operates on a numeric values. + +## Options + +This column type has no additional options. + +## Inherited options + + + + \ No newline at end of file diff --git a/docs/src/reference/types/filter/doctrine-orm/string.md b/docs/src/reference/types/filter/doctrine-orm/string.md new file mode 100644 index 00000000..2aee1119 --- /dev/null +++ b/docs/src/reference/types/filter/doctrine-orm/string.md @@ -0,0 +1,20 @@ + + +# Doctrine ORM StringFilterType + +The Doctrine ORM [`StringFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/Type/StringFilterType.php) represents a filter that operates on a string values. + +## Options + +This column type has no additional options. + +## Inherited options + + + + \ No newline at end of file diff --git a/docs/src/reference/types/filter/filter.md b/docs/src/reference/types/filter/filter.md new file mode 100644 index 00000000..ef8fbbee --- /dev/null +++ b/docs/src/reference/types/filter/filter.md @@ -0,0 +1,11 @@ + + +# FilterType + +The [`FilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/FilterType.php) represents a base filter used as a parent for every other filter type in the bundle. + +## Options + + diff --git a/docs/src/reference/types/filter/options/doctrine-orm.md b/docs/src/reference/types/filter/options/doctrine-orm.md new file mode 100644 index 00000000..8f000df9 --- /dev/null +++ b/docs/src/reference/types/filter/options/doctrine-orm.md @@ -0,0 +1,53 @@ +### `trim` + +- **type**: `bool` +- **default**: `false` + +Determines whether the `TRIM()` function should be applied on the expression. Uses the [`TrimExpressionTransformer`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/TrimExpressionTransformer.php) transformer. + +### `lower` + +- **type**: `bool` +- **default**: `false` + +Determines whether the `LOWER()` function should be applied on the expression. Uses the [`LowerExpressionTransformer`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/LowerExpressionTransformer.php) transformer. + +### `upper` + +- **type**: `bool` +- **default**: `false` + +Determines whether the `UPPER()` function should be applied on the expression. Uses the [`UpperExpressionTransformer`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/UpperExpressionTransformer.php) transformer. + +### `expression_transformers` + +- **type**: [`ExpressionTransformerInterface[]`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/ExpressionTransformerInterface.php) +- **default**: `[]` + +Defines expression transformers to apply on the expression. + +```php +use App\DataTable\Filter\ExpressionTransformer\UnaccentExpressionTransformer; +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\ExpressionTransformer\LowerExpressionTransformer; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\ExpressionTransformer\TrimExpressionTransformer; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType; + +class ProductDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + $builder + ->addFilter('name', TextFilterType::class, [ + 'expression_transformers' => [ + new LowerExpressionTransformer(), + new TrimExpressionTransformer(), + ], + ]) + ; + } +} +``` + +For more information about expression transformers, [read here](../../../../../docs/integrations/doctrine-orm/expression-transformers.md). diff --git a/docs/src/reference/types/filter/options/filter.md b/docs/src/reference/types/filter/options/filter.md new file mode 100644 index 00000000..b9c1a43f --- /dev/null +++ b/docs/src/reference/types/filter/options/filter.md @@ -0,0 +1,90 @@ + + +### `label` + +- **type**: `null`, `false`, `string` or `Symfony\Component\Translation\TranslatableInterface` +- **default**: `null` + +Sets the label that will be used when rendering the filter. + +When value is `null`, a sentence cased filter name is used as a label, for example: + +| Filter name | Guessed label | +|-------------|---------------| +| name | Name | +| firstName | First name | + +### `label_translation_parameters` + +- **type**: `array` +- **default**: `[]` + +Sets the parameters used when translating the `label` option. + +### `translation_domain` + +- **type**: `false` or `string` +- **default**: `'KreyuDataTable'` + +Sets the translation domain used when translating the translatable filter values. +Setting the option to `false` disables translation for the filter. + +### `query_path` + +- **type**: `null` or `string` +- **default**: `null` the query path is guessed from the filter name + +Sets the path used in the proxy query to perform the filtering on. + +### `form_type` + +- **type**: `string` +- **default**: `'{{ defaults.formType }}'` + +This is the form type used to render the filter value field. + +### `form_options` + +- **type**: `array` +- **default**: `{{ defaults.formOptions }}` + +This is the array that's passed to the form type specified in the `form_type` option. + +### `operator_form_type` + +- **type**: `string` +- **default**: `'Kreyu\Bundle\DataTableBundle\Filter\Form\Type\OperatorType'` + +This is the form type used to render the filter operator field. + +### `operator_form_options` + +- **type**: `array` +- **default**: `[]` + +This is the array that's passed to the form type specified in the `operator_form_type` option. + +### `operator_selectable` + +- **type**: `bool` +- **default**: `false` + +Determines whether the operator can be selected by the user. + +### `default_operator` + +- **type**: `Kreyu\Bundle\DataTableBundle\Filter\Operator` +- **default**: `{{ defaults.defaultOperator }}` + +Determines a default operator for the filter. diff --git a/docs/src/reference/types/filter/search.md b/docs/src/reference/types/filter/search.md new file mode 100644 index 00000000..fa644d02 --- /dev/null +++ b/docs/src/reference/types/filter/search.md @@ -0,0 +1,91 @@ + + +# SearchFilterType + +The [`SearchFilterType`](https://github.com/Kreyu/data-table-bundle/blob/main/src/Filter/Type/SearchFilterType.php) represents a special filter that is rendered on the outside of filtering form as a search input. + +![Search filter type](/search_filter_type.png) + +## Adding the search handler + +Instead of using this filter, you can use the `setSearchHandler()` method: + +```php +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface; + +class ProductDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + $builder->setSearchHandler(function (ProxyQueryInterface $query, string $search) { + // ... + }); + } +} +``` + +Defining a search handler automatically adds search filter. + +To disable this behavior, use the `setAutoAddingSearchFilter()` method: + +```php +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface; + +class ProductDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + $builder->setAutoAddingSearchFilter(false); + } +} +``` + +To override configuration of the automatically added filter, you can add search filter manually with the same name: + +```php +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface; +use Kreyu\Bundle\DataTableBundle\Filter\Type\SearchFilterType; + +class ProductDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + // Set "__search" as filter name or use constant: + $builder->addFilter(DataTableBuilderInterface::SEARCH_FILTER_NAME, SearchFilterType::class, [ + // ... + ]); + } +} +``` + +## Options + +### `handler` + +- **type**: `callable` + +Sets callable that operates on the query passed as a first argument: + +```php +use Kreyu\Bundle\DataTableBundle\Filter\Type\SearchFilterType; +use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface; + +$builder + ->addFilter('search', SearchFilterType::class, [ + 'handler' => function (ProxyQueryInterface $query, string $search): void { + // ... + }, + ]) +``` + +## Inherited options + + diff --git a/docs/static/batch_action.png b/docs/static/batch_action.png deleted file mode 100755 index 6f02e45ed281c3a15ed592dadb8bb3e313313689..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20523 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 cU+4gaf}*ht_jcD=4?lp3f%(NoJ@OV diff --git a/docs/static/global_action.png b/docs/static/global_action.png deleted file mode 100755 index 03647b2ebb12f5d373456c98f55997b5a806e2d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3379 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 n4&&b7x-$Yf 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 deleted file mode 100755 index 4708564f..00000000 --- a/docs/upgrade-guide/index.yml +++ /dev/null @@ -1 +0,0 @@ -visibility: hidden \ No newline at end of file diff --git a/phpstan.neon b/phpstan.dist.neon similarity index 83% rename from phpstan.neon rename to phpstan.dist.neon index fe607c2e..ab2834f1 100755 --- a/phpstan.neon +++ b/phpstan.dist.neon @@ -1,6 +1,9 @@ parameters: level: 5 checkGenericClassInNonGenericObjectType: false + paths: + - src + - tests excludePaths: - src/Resources/skeleton/* - src/DependencyInjection/Configuration.php \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..7c32da77 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,19 @@ + + + + + + tests + + + + + + + + src + + + diff --git a/src/AbstractExtension.php b/src/AbstractExtension.php index af80b060..e0357834 100644 --- a/src/AbstractExtension.php +++ b/src/AbstractExtension.php @@ -18,12 +18,12 @@ abstract class AbstractExtension /** * @var array */ - private array $types = []; + private array $types; /** * @var array> */ - private array $typeExtensions = []; + private array $typeExtensions; public function hasType(string $name): bool { @@ -114,13 +114,19 @@ private function initTypeExtensions(): void $typeExtensionClass = $this->getTypeExtensionClass(); - foreach ($this->loadTypeExtensions() as $extension) { - if (!$extension instanceof $typeExtensionClass) { - throw new UnexpectedTypeException($extension, $typeExtensionClass); + foreach ($this->loadTypeExtensions() as $extensions) { + if (!is_array($extensions)) { + $extensions = [$extensions]; } - foreach ($extension::getExtendedTypes() as $extendedType) { - $this->typeExtensions[$extendedType][] = $extension; + foreach ($extensions 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 index af34c0fc..60de0938 100644 --- a/src/AbstractRegistry.php +++ b/src/AbstractRegistry.php @@ -4,6 +4,7 @@ namespace Kreyu\Bundle\DataTableBundle; +use Kreyu\Bundle\DataTableBundle\Exception\ExceptionInterface; use Kreyu\Bundle\DataTableBundle\Exception\InvalidArgumentException; use Kreyu\Bundle\DataTableBundle\Exception\UnexpectedTypeException; @@ -42,6 +43,29 @@ public function __construct( } } + public function hasType(string $name): bool + { + if (isset($this->types[$name])) { + return true; + } + + try { + $this->doGetType($name); + } catch (ExceptionInterface) { + return false; + } + + return true; + } + + /** + * @return iterable + */ + public function getExtensions(): iterable + { + return $this->extensions; + } + /** * @return TResolvedType */ @@ -109,6 +133,11 @@ private function resolveType($type) } } + /** + * @return TResolvedType + */ + abstract public function getType(string $name); + /** * @return class-string */ diff --git a/src/Action/ActionRegistryInterface.php b/src/Action/ActionRegistryInterface.php index 18b04b6e..143647a3 100755 --- a/src/Action/ActionRegistryInterface.php +++ b/src/Action/ActionRegistryInterface.php @@ -4,6 +4,7 @@ namespace Kreyu\Bundle\DataTableBundle\Action; +use Kreyu\Bundle\DataTableBundle\Action\Extension\ActionExtensionInterface; use Kreyu\Bundle\DataTableBundle\Action\Type\ActionTypeInterface; use Kreyu\Bundle\DataTableBundle\Action\Type\ResolvedActionTypeInterface; @@ -13,4 +14,9 @@ interface ActionRegistryInterface * @param class-string $name */ public function getType(string $name): ResolvedActionTypeInterface; + + /** + * @return iterable + */ + public function getExtensions(): iterable; } diff --git a/src/Action/Extension/PreloadedActionExtension.php b/src/Action/Extension/PreloadedActionExtension.php index a05dd1b9..fa05d562 100644 --- a/src/Action/Extension/PreloadedActionExtension.php +++ b/src/Action/Extension/PreloadedActionExtension.php @@ -9,8 +9,8 @@ class PreloadedActionExtension extends AbstractActionExtension { /** - * @param array $types - * @param array> $typeExtensions + * @param array $types + * @param array|array> $typeExtensions */ public function __construct( private readonly array $types = [], diff --git a/src/Bridge/Doctrine/Orm/Event/DoctrineOrmFilterEvent.php b/src/Bridge/Doctrine/Orm/Event/DoctrineOrmFilterEvent.php new file mode 100644 index 00000000..69260d38 --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Event/DoctrineOrmFilterEvent.php @@ -0,0 +1,35 @@ +query; + } + + public function getData(): FilterData + { + return $this->data; + } + + public function getFilter(): FilterInterface + { + return $this->filter; + } +} diff --git a/src/Bridge/Doctrine/Orm/Event/DoctrineOrmFilterEvents.php b/src/Bridge/Doctrine/Orm/Event/DoctrineOrmFilterEvents.php new file mode 100644 index 00000000..82dbf8d5 --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Event/DoctrineOrmFilterEvents.php @@ -0,0 +1,19 @@ +expression; + } + + public function setExpression(mixed $expression): void + { + $this->expression = $expression; + } +} diff --git a/src/Bridge/Doctrine/Orm/Event/PreSetParametersEvent.php b/src/Bridge/Doctrine/Orm/Event/PreSetParametersEvent.php new file mode 100644 index 00000000..4130d4a1 --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Event/PreSetParametersEvent.php @@ -0,0 +1,35 @@ + + */ + public function getParameters(): array + { + return $this->parameters; + } + + public function setParameters(array $parameters): void + { + $this->parameters = $parameters; + } +} diff --git a/src/Bridge/Doctrine/Orm/EventListener/ApplyExpressionTransformers.php b/src/Bridge/Doctrine/Orm/EventListener/ApplyExpressionTransformers.php new file mode 100644 index 00000000..095e749d --- /dev/null +++ b/src/Bridge/Doctrine/Orm/EventListener/ApplyExpressionTransformers.php @@ -0,0 +1,44 @@ +getFilter(); + $expression = $event->getExpression(); + + if ($filter->getConfig()->getOption('trim')) { + $expression = (new TrimExpressionTransformer())->transform($expression); + } + + if ($filter->getConfig()->getOption('lower')) { + $expression = (new LowerExpressionTransformer())->transform($expression); + } + + if ($filter->getConfig()->getOption('upper')) { + $expression = (new UpperExpressionTransformer())->transform($expression); + } + + foreach ($filter->getConfig()->getOption('expression_transformers') as $expressionTransformer) { + $expression = $expressionTransformer->transform($expression); + } + + $event->setExpression($expression); + } + + public static function getSubscribedEvents(): array + { + return [DoctrineOrmFilterEvents::PRE_APPLY_EXPRESSION => 'preApplyExpression']; + } +} diff --git a/src/Bridge/Doctrine/Orm/EventListener/TransformDateRangeFilterData.php b/src/Bridge/Doctrine/Orm/EventListener/TransformDateRangeFilterData.php new file mode 100644 index 00000000..b70ec376 --- /dev/null +++ b/src/Bridge/Doctrine/Orm/EventListener/TransformDateRangeFilterData.php @@ -0,0 +1,52 @@ +getData(); + $value = $data->getValue(); + + $valueFrom = $value['from'] ?? null; + $valueTo = $value['to'] ?? null; + + if ($valueFrom) { + $valueFrom = \DateTime::createFromInterface($valueFrom); + $valueFrom->setTime(0, 0); + } + + if ($valueTo) { + $valueTo = \DateTime::createFromInterface($valueTo)->modify('+1 day'); + $valueTo->setTime(0, 0); + } + + $data = clone $data; + + if ($valueFrom && $valueTo) { + $data->setValue(['from' => $valueFrom, 'to' => $valueTo]); + $data->setOperator(Operator::Between); + } elseif ($valueFrom) { + $data->setValue($valueFrom); + $data->setOperator(Operator::GreaterThanEquals); + } elseif ($valueTo) { + $data->setValue($valueTo); + $data->setOperator(Operator::LessThan); + } + + $event->setData($data); + } + + public static function getSubscribedEvents(): array + { + return [FilterEvents::PRE_HANDLE => 'preHandle']; + } +} \ No newline at end of file diff --git a/src/Bridge/Doctrine/Orm/Filter/DoctrineOrmFilterHandler.php b/src/Bridge/Doctrine/Orm/Filter/DoctrineOrmFilterHandler.php new file mode 100644 index 00000000..19cf631b --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Filter/DoctrineOrmFilterHandler.php @@ -0,0 +1,65 @@ +parameterFactory->create($query, $data, $filter); + + $event = new PreSetParametersEvent($query, $data, $filter, $parameters); + + $this->dispatch(DoctrineOrmFilterEvents::PRE_SET_PARAMETERS, $event); + + $queryBuilder = $query->getQueryBuilder(); + + foreach ($event->getParameters() as $parameter) { + $queryBuilder->setParameter($parameter->getName(), $parameter->getValue(), $parameter->getType()); + } + + $expression = $this->expressionFactory->create($query, $data, $filter, $event->getParameters()); + + $event = new PreApplyExpressionEvent($query, $data, $filter, $expression); + + $this->dispatch(DoctrineOrmFilterEvents::PRE_APPLY_EXPRESSION, $event); + + $queryBuilder->andWhere($event->getExpression()); + } + + private function dispatch(string $eventName, DoctrineOrmFilterEvent $event): void + { + $dispatcher = $event->getFilter()->getConfig()->getEventDispatcher(); + + if ($dispatcher->hasListeners($eventName)) { + $dispatcher->dispatch($event, $eventName); + } + } +} diff --git a/src/Bridge/Doctrine/Orm/Filter/ExpressionFactory/ExpressionFactory.php b/src/Bridge/Doctrine/Orm/Filter/ExpressionFactory/ExpressionFactory.php new file mode 100644 index 00000000..a258f44b --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Filter/ExpressionFactory/ExpressionFactory.php @@ -0,0 +1,56 @@ +getAliasResolver()->resolve($filter->getQueryPath(), $query->getQueryBuilder()); + + $operator = $data->getOperator() ?? $filter->getConfig()->getDefaultOperator(); + + $expr = new Expr(); + + if (Operator::Between === $operator) { + $parameterFrom = $parameters['from'] ?? null; + $parameterTo = $parameters['to'] ?? null; + + if ($parameterFrom && $parameterTo) { + return $expr->between($queryPath, ':'.$parameterFrom->getName(), ':'.$parameterTo->getName()); + } + + throw new InvalidArgumentException('Operator "between" requires "from" and "to" parameters.'); + } + + $exprMethod = 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(...), + Operator::Contains, Operator::StartsWith, Operator::EndsWith => $expr->like(...), + Operator::NotContains => $expr->notLike(...), + Operator::In => $expr->in(...), + Operator::NotIn => $expr->notIn(...), + }; + + $parameter = $parameters[array_key_first($parameters)]; + + return $exprMethod($queryPath, ':'.$parameter->getName()); + } +} diff --git a/src/Bridge/Doctrine/Orm/Filter/ExpressionFactory/ExpressionFactoryInterface.php b/src/Bridge/Doctrine/Orm/Filter/ExpressionFactory/ExpressionFactoryInterface.php new file mode 100644 index 00000000..80517803 --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Filter/ExpressionFactory/ExpressionFactoryInterface.php @@ -0,0 +1,18 @@ + $parameters + */ + public function create(DoctrineOrmProxyQueryInterface $query, FilterData $data, FilterInterface $filter, array $parameters): mixed; +} diff --git a/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/AbstractComparisonExpressionTransformer.php b/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/AbstractComparisonExpressionTransformer.php new file mode 100644 index 00000000..c0543879 --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/AbstractComparisonExpressionTransformer.php @@ -0,0 +1,53 @@ +getLeftExpr(); + $rightExpr = $expression->getRightExpr(); + + if ($this->transformLeftExpr) { + $leftExpr = $this->transformLeftExpr($leftExpr); + } + + if ($this->transformRightExpr) { + $rightExpr = $this->transformRightExpr($rightExpr); + } + + return new Comparison($leftExpr, $expression->getOperator(), $rightExpr); + } + + protected function transformLeftExpr(mixed $leftExpr): mixed + { + return $leftExpr; + } + + protected function transformRightExpr(mixed $rightExpr): mixed + { + return $rightExpr; + } + + protected function getExpressionBuilder(): Expr + { + return new Expr(); + } +} diff --git a/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/CallbackExpressionTransformer.php b/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/CallbackExpressionTransformer.php new file mode 100644 index 00000000..3a9dfee1 --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/CallbackExpressionTransformer.php @@ -0,0 +1,20 @@ +callback = $callback(...); + } + + public function transform(mixed $expression): mixed + { + return ($this->callback)($expression); + } +} diff --git a/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/ExpressionTransformerInterface.php b/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/ExpressionTransformerInterface.php new file mode 100644 index 00000000..76de5313 --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/ExpressionTransformerInterface.php @@ -0,0 +1,10 @@ +getExpressionBuilder()->lower($leftExpr); + } + + protected function transformRightExpr(mixed $rightExpr): Expr\Func + { + return $this->getExpressionBuilder()->lower($rightExpr); + } +} diff --git a/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/TrimExpressionTransformer.php b/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/TrimExpressionTransformer.php new file mode 100644 index 00000000..c4c34862 --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/TrimExpressionTransformer.php @@ -0,0 +1,20 @@ +getExpressionBuilder()->trim($leftExpr); + } + + protected function transformRightExpr(mixed $rightExpr): Expr\Func + { + return $this->getExpressionBuilder()->trim($rightExpr); + } +} diff --git a/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/UpperExpressionTransformer.php b/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/UpperExpressionTransformer.php new file mode 100644 index 00000000..6c9ce311 --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/UpperExpressionTransformer.php @@ -0,0 +1,20 @@ +getExpressionBuilder()->upper($leftExpr); + } + + protected function transformRightExpr(mixed $rightExpr): Expr\Func + { + return $this->getExpressionBuilder()->upper($rightExpr); + } +} diff --git a/src/Bridge/Doctrine/Orm/Filter/Extension/DoctrineOrmFilterExtension.php b/src/Bridge/Doctrine/Orm/Filter/Extension/DoctrineOrmFilterExtension.php new file mode 100644 index 00000000..a2fd756a --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Filter/Extension/DoctrineOrmFilterExtension.php @@ -0,0 +1,32 @@ +getValue(); + + if ($value instanceof \DateTimeInterface) { + return $value->format($filter->getConfig()->getOption('form_options')['input_format'] ?? 'Y-m-d'); + } + + return (string) $value; + } +} diff --git a/src/Bridge/Doctrine/Orm/Filter/Formatter/DateRangeActiveFilterFormatter.php b/src/Bridge/Doctrine/Orm/Filter/Formatter/DateRangeActiveFilterFormatter.php new file mode 100644 index 00000000..151b15b6 --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Filter/Formatter/DateRangeActiveFilterFormatter.php @@ -0,0 +1,33 @@ +getValue(); + + $dateFrom = $value['from']; + $dateTo = $value['to']; + + if (null !== $dateFrom && null === $dateTo) { + return new TranslatableMessage('After %date%', ['%date%' => $dateFrom->format('Y-m-d')], 'KreyuDataTable'); + } + + if (null === $dateFrom && null !== $dateTo) { + return new TranslatableMessage('Before %date%', ['%date%' => $dateTo->format('Y-m-d')], 'KreyuDataTable'); + } + + if ($dateFrom == $dateTo) { + return $dateFrom->format('Y-m-d'); + } + + return $dateFrom->format('Y-m-d').' - '.$dateTo->format('Y-m-d'); + } +} diff --git a/src/Bridge/Doctrine/Orm/Filter/Formatter/DateTimeActiveFilterFormatter.php b/src/Bridge/Doctrine/Orm/Filter/Formatter/DateTimeActiveFilterFormatter.php new file mode 100644 index 00000000..8768787a --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Filter/Formatter/DateTimeActiveFilterFormatter.php @@ -0,0 +1,38 @@ +getValue(); + + if ($value instanceof \DateTimeInterface) { + $formOptions = $filter->getConfig()->getOption('form_options'); + + $format = $formOptions['input_format'] ?? null; + + if (null === $format) { + $format = 'Y-m-d H'; + + if ($formOptions['with_minutes'] ?? true) { + $format .= ':i'; + } + + if ($formOptions['with_seconds'] ?? true) { + $format .= ':s'; + } + } + + return $value->format($format); + } + + return (string) $value; + } +} diff --git a/src/Bridge/Doctrine/Orm/Filter/Formatter/EntityActiveFilterFormatter.php b/src/Bridge/Doctrine/Orm/Filter/Formatter/EntityActiveFilterFormatter.php new file mode 100644 index 00000000..57c4364b --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Filter/Formatter/EntityActiveFilterFormatter.php @@ -0,0 +1,27 @@ +getConfig()->getOption('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/ParameterFactory/ParameterFactory.php b/src/Bridge/Doctrine/Orm/Filter/ParameterFactory/ParameterFactory.php new file mode 100644 index 00000000..58ad248d --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Filter/ParameterFactory/ParameterFactory.php @@ -0,0 +1,45 @@ +getName().'_'.$query->getUniqueParameterId(); + + $value = $data->getValue(); + $operator = $data->getOperator(); + + if (Operator::Between === $operator) { + if (null !== $from = $value['from'] ?? null) { + $parameters['from'] = new Parameter($name.'_from', $from); + } + + if (null !== $to = $value['to'] ?? null) { + $parameters['to'] = new Parameter($name.'_to', $to); + } + + return $parameters; + } + + $parameters[] = new Parameter($name, match ($operator) { + Operator::Contains, Operator::NotContains => "%$value%", + Operator::StartsWith => "$value%", + Operator::EndsWith => "%$value", + default => $value, + }); + + return $parameters; + } +} diff --git a/src/Bridge/Doctrine/Orm/Filter/ParameterFactory/ParameterFactoryInterface.php b/src/Bridge/Doctrine/Orm/Filter/ParameterFactory/ParameterFactoryInterface.php new file mode 100644 index 00000000..bb3cb905 --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Filter/ParameterFactory/ParameterFactoryInterface.php @@ -0,0 +1,18 @@ + + */ + public function create(DoctrineOrmProxyQueryInterface $query, FilterData $data, FilterInterface $filter): array; +} diff --git a/src/Bridge/Doctrine/Orm/Filter/Type/AbstractDoctrineOrmFilterType.php b/src/Bridge/Doctrine/Orm/Filter/Type/AbstractDoctrineOrmFilterType.php index c411b4ed..60b6e74f 100644 --- a/src/Bridge/Doctrine/Orm/Filter/Type/AbstractDoctrineOrmFilterType.php +++ b/src/Bridge/Doctrine/Orm/Filter/Type/AbstractDoctrineOrmFilterType.php @@ -4,91 +4,10 @@ namespace Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type; -use Doctrine\ORM\Query\Expr; -use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQueryInterface; -use Kreyu\Bundle\DataTableBundle\Exception\InvalidArgumentException; -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\Filter\Type\AbstractFilterType; -use Kreyu\Bundle\DataTableBundle\Filter\Type\FilterTypeInterface; -use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface; -/** - * @extends FilterTypeInterface - */ 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 deleted file mode 100755 index 1d8d2499..00000000 --- a/src/Bridge/Doctrine/Orm/Filter/Type/AbstractFilterType.php +++ /dev/null @@ -1,12 +0,0 @@ -setSupportedOperators([ + Operator::Equals, + Operator::NotEquals, + ]); + } + public function configureOptions(OptionsResolver $resolver): void { $resolver ->setDefaults([ 'form_type' => ChoiceType::class, - 'active_filter_formatter' => function (FilterData $data): TranslatableInterface { - return t($data->getValue() ? 'Yes' : 'No', domain: 'KreyuDataTable'); + 'active_filter_formatter' => function (FilterData $data) { + return new TranslatableMessage($data->getValue() ? 'Yes' : 'No', domain: 'KreyuDataTable'); }, ]) - ->addNormalizer('form_options', function (Options $options, mixed $value) { + ->addNormalizer('form_options', function (Options $options, array $value): array { 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'); - }, + 'choices' => ['Yes' => true, 'No' => false], + 'choice_translation_domain' => 'KreyuDataTable', ]; }) ; } - - protected function getOperatorExpression(string $queryPath, string $parameterName, Operator $operator, Expr $expr): object - { - $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 100755 new mode 100644 index f34a6d0b..1b2a0b89 --- a/src/Bridge/Doctrine/Orm/Filter/Type/CallbackFilterType.php +++ b/src/Bridge/Doctrine/Orm/Filter/Type/CallbackFilterType.php @@ -12,6 +12,9 @@ use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @deprecated since 0.15, use {@see \Kreyu\Bundle\DataTableBundle\Filter\Type\CallbackFilterType} instead + */ class CallbackFilterType extends AbstractDoctrineOrmFilterType { public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter, array $options): void diff --git a/src/Bridge/Doctrine/Orm/Filter/Type/DateFilterType.php b/src/Bridge/Doctrine/Orm/Filter/Type/DateFilterType.php old mode 100755 new mode 100644 index 5bd1f1da..5ad3baae --- a/src/Bridge/Doctrine/Orm/Filter/Type/DateFilterType.php +++ b/src/Bridge/Doctrine/Orm/Filter/Type/DateFilterType.php @@ -4,101 +4,41 @@ namespace Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type; -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\FilterBuilderInterface; use Kreyu\Bundle\DataTableBundle\Filter\Operator; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Formatter\DateActiveFilterFormatter; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class DateFilterType extends AbstractDoctrineOrmFilterType { + public function buildFilter(FilterBuilderInterface $builder, array $options): void + { + $builder->setSupportedOperators([ + Operator::Equals, + Operator::NotEquals, + Operator::GreaterThan, + Operator::GreaterThanEquals, + Operator::LessThan, + Operator::LessThanEquals, + ]); + } + public function configureOptions(OptionsResolver $resolver): void { $resolver ->setDefaults([ 'form_type' => DateType::class, - 'supported_operators' => [ - Operator::Equals, - Operator::NotEquals, - Operator::GreaterThan, - Operator::GreaterThanEquals, - Operator::LessThan, - Operator::LessThanEquals, - ], - 'active_filter_formatter' => $this->getFormattedActiveFilterString(...), + 'active_filter_formatter' => new DateActiveFilterFormatter(), ]) - ->addNormalizer('form_options', function (Options $options, array $value): array { + ->addNormalizer('form_options', function (Options $options, array $value) { 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 => '', - }; - }) ; } - - 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', $value); - } elseif (is_array($value)) { - $dateTime = (new \DateTime())->setDate( - year: (int) $value['date']['year'] ?: 0, - month: (int) $value['date']['month'] ?: 0, - 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))); - } - - $dateTime = \DateTime::createFromInterface($dateTime); - $dateTime->setTime(0, 0); - - 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 100755 new mode 100644 index 2a49ae61..3df98598 --- a/src/Bridge/Doctrine/Orm/Filter/Type/DateRangeFilterType.php +++ b/src/Bridge/Doctrine/Orm/Filter/Type/DateRangeFilterType.php @@ -4,62 +4,25 @@ namespace Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type; -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\FilterBuilderInterface; use Kreyu\Bundle\DataTableBundle\Filter\Form\Type\DateRangeType; -use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface; +use Kreyu\Bundle\DataTableBundle\Filter\Operator; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\EventListener\TransformDateRangeFilterData; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Formatter\DateRangeActiveFilterFormatter; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Translation\TranslatableMessage; - -use function Symfony\Component\Translation\t; class DateRangeFilterType extends AbstractDoctrineOrmFilterType { - public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter, array $options): void + public function buildFilter(FilterBuilderInterface $builder, array $options): void { - if (!$query instanceof DoctrineOrmProxyQueryInterface) { - throw new UnexpectedTypeException($query, DoctrineOrmProxyQueryInterface::class); - } - - $value = $data->getValue(); - - $parameterName = $this->getUniqueParameterName($query, $filter); - - $queryPath = $this->getFilterQueryPath($query, $filter); - - $criteria = $query->expr()->andX(); - - if (!is_array($value)) { - return; - } - - if (null !== $dateFrom = $value['from'] ?? null) { - $parameterNameFrom = $parameterName.'_from'; - - $dateFrom = \DateTime::createFromInterface($dateFrom); - $dateFrom->setTime(0, 0); - - $criteria->add($query->expr()->gte($queryPath, ":$parameterNameFrom")); - - $query->setParameter($parameterNameFrom, $dateFrom); - } - - if (null !== $valueTo = $value['to'] ?? null) { - $parameterNameTo = $parameterName.'_to'; - - $valueTo = \DateTime::createFromInterface($valueTo)->modify('+1 day'); - $valueTo->setTime(0, 0); - - $criteria->add($query->expr()->lt($queryPath, ":$parameterNameTo")); - - $query->setParameter($parameterNameTo, $valueTo); - } + $builder + ->setOperatorSelectable(false) + ->setDefaultOperator(Operator::Between) + ; - if ($criteria->count() > 0) { - $query->andWhere($criteria); - } + $builder + ->addEventSubscriber(new TransformDateRangeFilterData()) + ; } public function configureOptions(OptionsResolver $resolver): void @@ -67,31 +30,8 @@ public function configureOptions(OptionsResolver $resolver): void $resolver ->setDefaults([ 'form_type' => DateRangeType::class, - 'active_filter_formatter' => $this->getFormattedActiveFilterString(...), - 'empty_data' => ['from' => '', 'to' => ''], + 'active_filter_formatter' => new DateRangeActiveFilterFormatter(), ]) ; } - - public function getFormattedActiveFilterString(FilterData $data): string|TranslatableMessage - { - $value = $data->getValue(); - - $dateFrom = $value['from']; - $dateTo = $value['to']; - - if (null !== $dateFrom && null === $dateTo) { - return t('After %date%', ['%date%' => $dateFrom->format('Y-m-d')], 'KreyuDataTable'); - } - - if (null === $dateFrom && null !== $dateTo) { - return t('Before %date%', ['%date%' => $dateTo->format('Y-m-d')], 'KreyuDataTable'); - } - - if ($dateFrom == $dateTo) { - return $dateFrom->format('Y-m-d'); - } - - return $dateFrom->format('Y-m-d').' - '.$dateTo->format('Y-m-d'); - } } diff --git a/src/Bridge/Doctrine/Orm/Filter/Type/DateTimeFilterType.php b/src/Bridge/Doctrine/Orm/Filter/Type/DateTimeFilterType.php old mode 100755 new mode 100644 index dc0d173f..11273e99 --- a/src/Bridge/Doctrine/Orm/Filter/Type/DateTimeFilterType.php +++ b/src/Bridge/Doctrine/Orm/Filter/Type/DateTimeFilterType.php @@ -4,31 +4,33 @@ namespace Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type; -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\FilterBuilderInterface; use Kreyu\Bundle\DataTableBundle\Filter\Operator; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Formatter\DateTimeActiveFilterFormatter; use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class DateTimeFilterType extends AbstractDoctrineOrmFilterType { + public function buildFilter(FilterBuilderInterface $builder, array $options): void + { + $builder->setSupportedOperators([ + Operator::Equals, + Operator::NotEquals, + Operator::GreaterThan, + Operator::GreaterThanEquals, + Operator::LessThan, + Operator::LessThanEquals, + ]); + } + public function configureOptions(OptionsResolver $resolver): void { $resolver ->setDefaults([ 'form_type' => DateTimeType::class, - 'supported_operators' => [ - Operator::Equals, - Operator::NotEquals, - Operator::GreaterThan, - Operator::GreaterThanEquals, - Operator::LessThan, - Operator::LessThanEquals, - ], - 'active_filter_formatter' => $this->getFormattedActiveFilterString(...), + 'active_filter_formatter' => new DateTimeActiveFilterFormatter(), ]) ->addNormalizer('form_options', function (Options $options, array $value): array { if (DateTimeType::class !== $options['form_type']) { @@ -37,90 +39,6 @@ public function configureOptions(OptionsResolver $resolver): void return $value + ['widget' => 'single_text']; }) - ->addNormalizer('empty_data', function (Options $options, string|array $value): string|array { - if (DateTimeType::class !== $options['form_type']) { - 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 => '', - }; - }) ; } - - protected function getFilterValue(FilterData $data): \DateTimeInterface - { - $value = $data->getValue(); - - if ($value instanceof \DateTimeInterface) { - 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, - day: (int) $value['date']['day'] ?: 0, - ) - ->setTime( - hour: (int) $value['time']['hour'] ?: 0, - minute: (int) $value['time']['minute'] ?: 0, - second: (int) $value['time']['second'] ?: 0, - ) - ; - } - - 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 old mode 100755 new mode 100644 index 1e175087..d3ca1a09 --- a/src/Bridge/Doctrine/Orm/Filter/Type/DoctrineOrmFilterType.php +++ b/src/Bridge/Doctrine/Orm/Filter/Type/DoctrineOrmFilterType.php @@ -4,23 +4,34 @@ namespace Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type; -use Kreyu\Bundle\DataTableBundle\Filter\FilterData; -use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface; +use Kreyu\Bundle\DataTableBundle\Filter\FilterBuilderInterface; use Kreyu\Bundle\DataTableBundle\Filter\Type\AbstractFilterType; -use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\EventListener\ApplyExpressionTransformers; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\DoctrineOrmFilterHandler; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\ExpressionTransformer\ExpressionTransformerInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -class DoctrineOrmFilterType extends AbstractFilterType +final class DoctrineOrmFilterType extends AbstractFilterType { - public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter, array $options): void + public function buildFilter(FilterBuilderInterface $builder, array $options): void { + $builder->setHandler(new DoctrineOrmFilterHandler()); + $builder->addEventSubscriber(new ApplyExpressionTransformers()); } public function configureOptions(OptionsResolver $resolver): void { $resolver - ->setDefault('auto_alias_resolving', true) - ->setAllowedTypes('auto_alias_resolving', 'bool') + ->setDefaults([ + 'trim' => false, + 'lower' => false, + 'upper' => false, + 'expression_transformers' => [], + ]) + ->setAllowedTypes('trim', 'bool') + ->setAllowedTypes('lower', 'bool') + ->setAllowedTypes('upper', 'bool') + ->setAllowedTypes('expression_transformers', ExpressionTransformerInterface::class.'[]') ; } } diff --git a/src/Bridge/Doctrine/Orm/Filter/Type/EntityFilterType.php b/src/Bridge/Doctrine/Orm/Filter/Type/EntityFilterType.php old mode 100755 new mode 100644 index 54426447..1bd17622 --- a/src/Bridge/Doctrine/Orm/Filter/Type/EntityFilterType.php +++ b/src/Bridge/Doctrine/Orm/Filter/Type/EntityFilterType.php @@ -4,89 +4,61 @@ namespace Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type; -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 Doctrine\Persistence\ManagerRegistry; +use Kreyu\Bundle\DataTableBundle\Filter\FilterBuilderInterface; use Kreyu\Bundle\DataTableBundle\Filter\Operator; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Formatter\EntityActiveFilterFormatter; 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 AbstractDoctrineOrmFilterType { public function __construct( - private readonly Registry $doctrineRegistry, + private readonly ?ManagerRegistry $managerRegistry = null, ) { } + public function buildFilter(FilterBuilderInterface $builder, array $options): void + { + $builder->setSupportedOperators([ + Operator::Equals, + Operator::NotEquals, + Operator::In, + Operator::NotIn, + ]); + } + public function configureOptions(OptionsResolver $resolver): void { $resolver ->setDefaults([ 'form_type' => EntityType::class, - 'supported_operators' => [ - Operator::Equals, - Operator::NotEquals, - Operator::Contains, - Operator::NotContains, - ], 'choice_label' => null, - 'active_filter_formatter' => $this->getFormattedActiveFilterString(...), + 'active_filter_formatter' => new EntityActiveFilterFormatter(), ]) ->setAllowedTypes('choice_label', ['null', 'string', 'callable']) - ->addNormalizer('form_options', function (Options $options, array $value) { - if (EntityType::class !== $options['form_type']) { - return $value; - } - - // 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 (1 === count($identifiers)) { - $value += ['choice_value' => reset($identifiers)]; - } - } - - return $value; - }) ; - } - protected function getOperatorExpression(string $queryPath, string $parameterName, Operator $operator, Expr $expr): object - { - $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"); - } + // The persistence feature is saving the identifier of the entity, not the entire selected entity. + // Therefore, the EntityType requires "choice_value" option with a name of the entity identifier field. + if (null !== $this->managerRegistry) { + $resolver->addNormalizer('form_options', function (Options $options, array $formOptions) { + if (EntityType::class !== $options['form_type'] || null === $class = $formOptions['class'] ?? null) { + return $formOptions; + } - private function getFormattedActiveFilterString(FilterData $data, FilterInterface $filter, array $options): string - { - $choiceLabel = $options['choice_label']; + $identifiers = $this->managerRegistry + ->getManagerForClass($class) + ?->getClassMetadata($class) + ->getIdentifier() ?? []; - if (is_string($choiceLabel)) { - return PropertyAccess::createPropertyAccessor()->getValue($data->getValue(), $choiceLabel); - } + if (1 === count($identifiers)) { + $formOptions += ['choice_value' => reset($identifiers)]; + } - if (is_callable($choiceLabel)) { - return $choiceLabel($data->getValue()); + return $formOptions; + }); } - - 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 100755 new mode 100644 index ece09469..f948c748 --- a/src/Bridge/Doctrine/Orm/Filter/Type/NumericFilterType.php +++ b/src/Bridge/Doctrine/Orm/Filter/Type/NumericFilterType.php @@ -4,43 +4,29 @@ namespace Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type; -use Doctrine\ORM\Query\Expr; -use Kreyu\Bundle\DataTableBundle\Exception\InvalidArgumentException; +use Kreyu\Bundle\DataTableBundle\Filter\FilterBuilderInterface; use Kreyu\Bundle\DataTableBundle\Filter\Operator; use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\OptionsResolver\OptionsResolver; class NumericFilterType extends AbstractDoctrineOrmFilterType { - public function configureOptions(OptionsResolver $resolver): void + public function buildFilter(FilterBuilderInterface $builder, array $options): void { - $resolver - ->setDefaults([ - 'form_type' => NumberType::class, - 'supported_operators' => [ - Operator::Equals, - Operator::NotEquals, - Operator::GreaterThanEquals, - Operator::GreaterThan, - Operator::LessThanEquals, - Operator::LessThan, - ], - ]) - ; + $builder->setSupportedOperators([ + Operator::Equals, + Operator::NotEquals, + Operator::Contains, + Operator::NotContains, + Operator::StartsWith, + Operator::EndsWith, + ]); } - protected function getOperatorExpression(string $queryPath, string $parameterName, Operator $operator, Expr $expr): object + public function configureOptions(OptionsResolver $resolver): void { - $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"); + $resolver->setDefaults([ + 'form_type' => NumberType::class, + ]); } } diff --git a/src/Bridge/Doctrine/Orm/Filter/Type/StringFilterType.php b/src/Bridge/Doctrine/Orm/Filter/Type/StringFilterType.php old mode 100755 new mode 100644 index 8b289076..45a51799 --- a/src/Bridge/Doctrine/Orm/Filter/Type/StringFilterType.php +++ b/src/Bridge/Doctrine/Orm/Filter/Type/StringFilterType.php @@ -4,50 +4,28 @@ namespace Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type; -use Doctrine\ORM\Query\Expr; -use Kreyu\Bundle\DataTableBundle\Exception\InvalidArgumentException; +use Kreyu\Bundle\DataTableBundle\Filter\FilterBuilderInterface; use Kreyu\Bundle\DataTableBundle\Filter\Operator; use Symfony\Component\OptionsResolver\OptionsResolver; class StringFilterType extends AbstractDoctrineOrmFilterType { - public function configureOptions(OptionsResolver $resolver): void - { - $resolver - ->setDefaults([ - 'default_operator' => Operator::Contains, - 'supported_operators' => [ - Operator::Equals, - Operator::NotEquals, - Operator::Contains, - Operator::NotContains, - Operator::StartsWith, - Operator::EndsWith, - ], - ]) - ; - } - - protected function getOperatorExpression(string $queryPath, string $parameterName, Operator $operator, Expr $expr): object + public function buildFilter(FilterBuilderInterface $builder, array $options): void { - $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"); + $builder->setSupportedOperators([ + Operator::Equals, + Operator::NotEquals, + Operator::Contains, + Operator::NotContains, + Operator::StartsWith, + Operator::EndsWith, + ]); } - protected function getParameterValue(Operator $operator, mixed $value): string + public function configureOptions(OptionsResolver $resolver): void { - return (string) match ($operator) { - Operator::Contains, Operator::NotContains => "%$value%", - Operator::StartsWith => "$value%", - Operator::EndsWith => "%$value", - default => $value, - }; + $resolver->setDefaults([ + 'default_operator' => Operator::Contains, + ]); } } diff --git a/src/Bridge/Doctrine/Orm/Paginator/PaginatorFactory.php b/src/Bridge/Doctrine/Orm/Paginator/PaginatorFactory.php new file mode 100644 index 00000000..2a9c2cb8 --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Paginator/PaginatorFactory.php @@ -0,0 +1,131 @@ +getRootEntities()); + + if (false === $rootEntity) { + throw new \RuntimeException('There are no root entities defined in the query.'); + } + + $identifierFieldNames = $queryBuilder + ->getEntityManager() + ->getClassMetadata($rootEntity) + ->getIdentifierFieldNames(); + + $hasSingleIdentifierName = 1 === \count($identifierFieldNames); + $hasJoins = \count($queryBuilder->getDQLPart('join')) > 0; + + $query = $queryBuilder->getQuery(); + + if (!$hasJoins) { + $query->setHint(CountWalker::HINT_DISTINCT, false); + } + + foreach ($hints as $name => $value) { + $query->setHint($name, $value); + } + + // Paginator with fetchJoinCollection doesn't work with composite primary keys + // https://github.com/doctrine/orm/issues/2910 + // To stay safe fetch join only when we have single primary key and joins + $paginator = new Paginator($query, $hasSingleIdentifierName && $hasJoins); + + // it is only safe to disable output walkers for really simple queries + if ($this->canDisableOutputWalkers($queryBuilder)) { + $paginator->setUseOutputWalkers(false); + } + + return $paginator; + } + + /** + * @see https://github.com/doctrine/orm/issues/8278#issue-705517756 + */ + public function canDisableOutputWalkers(QueryBuilder $queryBuilder): bool + { + // Do not support queries using HAVING + if (null !== $queryBuilder->getDQLPart('having')) { + return false; + } + + $fromParts = $queryBuilder->getDQLPart('from'); + + // Do not support queries using multiple entities in FROM + if (1 !== \count($fromParts)) { + return false; + } + + $fromPart = current($fromParts); + + $classMetadata = $queryBuilder + ->getEntityManager() + ->getClassMetadata($fromPart->getFrom()); + + $identifierFieldNames = $classMetadata->getIdentifierFieldNames(); + + // Do not support entities using a composite identifier + if (1 !== \count($identifierFieldNames)) { + return false; + } + + $identifierName = current($identifierFieldNames); + + // Do not support entities using a foreign key as identifier + if ($classMetadata->hasAssociation($identifierName)) { + return false; + } + + // Do not support queries using a field from a toMany relation in the ORDER BY clause + if ($this->hasOrderByAssociation($queryBuilder)) { + return false; + } + + return true; + } + + public function hasOrderByAssociation(QueryBuilder $queryBuilder): bool + { + $joinParts = $queryBuilder->getDQLPart('join'); + + if (0 === \count($joinParts)) { + return false; + } + + $orderByParts = $queryBuilder->getDQLPart('orderBy'); + + if (empty($orderByParts)) { + return false; + } + + $joinAliases = []; + + foreach ($joinParts as $joinPart) { + foreach ($joinPart as $join) { + $joinAliases[] = $join->getAlias(); + } + } + + foreach ($orderByParts as $orderByPart) { + foreach ($orderByPart->getParts() as $part) { + foreach ($joinAliases as $joinAlias) { + if (str_starts_with($part, $joinAlias.'.')) { + return true; + } + } + } + } + + return false; + } +} diff --git a/src/Bridge/Doctrine/Orm/Paginator/PaginatorFactoryInterface.php b/src/Bridge/Doctrine/Orm/Paginator/PaginatorFactoryInterface.php new file mode 100644 index 00000000..18c076ef --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Paginator/PaginatorFactoryInterface.php @@ -0,0 +1,13 @@ +isResolvable($queryPath, $queryBuilder)) { + $queryPath = current($queryBuilder->getRootAliases()).'.'.$queryPath; + } + + return $queryPath; + } + + private function isResolvable(string $path, QueryBuilder $queryBuilder): bool + { + if (str_contains($path, '.')) { + return false; + } + + foreach ($queryBuilder->getDQLPart('select') ?? [] as $select) { + $parts = preg_split('/ as( hidden)? /i', $select->getParts()[0]); + + if ($path === ($parts[1] ?? $parts[0])) { + return false; + } + } + + return true; + } +} diff --git a/src/Bridge/Doctrine/Orm/Query/AliasResolverInterface.php b/src/Bridge/Doctrine/Orm/Query/AliasResolverInterface.php new file mode 100644 index 00000000..d9cb93bc --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Query/AliasResolverInterface.php @@ -0,0 +1,12 @@ + $hints @@ -38,39 +39,20 @@ public function __call(string $name, array $args): mixed return $this->queryBuilder->$name(...$args); } - public function __get(string $name): mixed - { - return $this->queryBuilder->{$name}; - } - public function __clone(): void { $this->queryBuilder = clone $this->queryBuilder; } - public function getQueryBuilder(): QueryBuilder - { - return $this->queryBuilder; - } - public function sort(SortingData $sortingData): void { - $rootAlias = current($this->queryBuilder->getRootAliases()); - - if (false === $rootAlias) { - throw new \RuntimeException('There are no root aliases defined in the query.'); - } - $this->queryBuilder->resetDQLPart('orderBy'); - foreach ($sortingData->getColumns() as $column) { - $propertyPath = (string) $column->getPropertyPath(); - - if ($rootAlias && !str_contains($propertyPath, '.') && !str_starts_with($propertyPath, '__')) { - $propertyPath = $rootAlias.'.'.$propertyPath; - } - - $this->queryBuilder->addOrderBy($propertyPath, $column->getDirection()); + foreach ($sortingData->getColumns() as $sortCriterion) { + $this->queryBuilder->addOrderBy( + $this->getAliasResolver()->resolve((string) $sortCriterion->getPropertyPath(), $this->queryBuilder), + $sortCriterion->getDirection(), + ); } } @@ -82,58 +64,17 @@ public function paginate(PaginationData $paginationData): void ; } - /** - * @throws \Exception - */ - public function getPagination(): PaginationInterface + public function getResult(): ResultSetInterface { - $maxResults = $this->queryBuilder->getMaxResults(); + $paginator = $this->getPaginatorFactory()->create($this->queryBuilder, $this->hints); + $paginator->getQuery()->setHydrationMode($this->hydrationMode); - $paginator = $this->createPaginator(forceDisabledFetchJoinCollection: null === $maxResults); - - try { - return new Pagination( - items: $paginator->getIterator(), - currentPageNumber: $this->getCurrentPageNumber(), - totalItemCount: $paginator->count(), - itemNumberPerPage: $maxResults, - ); - } catch (CurrentPageOutOfRangeException) { - $this->queryBuilder->setFirstResult(null); - } - - return $this->getPagination(); + return $this->getResultSetFactory()->create($paginator, $this->batchSize); } - public function getItems(): iterable + public function getQueryBuilder(): QueryBuilder { - $paginator = $this->createPaginator(forceDisabledFetchJoinCollection: true); - - $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); + return $this->queryBuilder; } public function getUniqueParameterId(): int @@ -141,24 +82,24 @@ public function getUniqueParameterId(): int return $this->uniqueParameterId++; } - public function setHint(string $name, mixed $value): void + public function getHints(): array { - $this->hints[$name] = $value; + return $this->hints; } - public function setHydrationMode(int|string $hydrationMode): void + public function setHint(string $name, mixed $value): void { - $this->hydrationMode = $hydrationMode; + $this->hints[$name] = $value; } - public function isEntityManagerClearingEnabled(): bool + public function getHydrationMode(): int|string { - return $this->entityManagerClearingEnabled; + return $this->hydrationMode; } - public function setEntityManagerClearingEnabled(bool $entityManagerClearingEnabled): void + public function setHydrationMode(int|string $hydrationMode): void { - $this->entityManagerClearingEnabled = $entityManagerClearingEnabled; + $this->hydrationMode = $hydrationMode; } public function getBatchSize(): int @@ -168,52 +109,40 @@ public function getBatchSize(): int public function setBatchSize(int $batchSize): void { + if ($batchSize <= 0) { + throw new InvalidArgumentException('The batch size must be positive.'); + } + $this->batchSize = $batchSize; } - private function getCurrentPageNumber(): int + public function getPaginatorFactory(): PaginatorFactoryInterface { - $firstResult = $this->queryBuilder->getFirstResult(); - $maxResults = $this->queryBuilder->getMaxResults() ?? 1; - - return (int) ($firstResult / $maxResults) + 1; + return $this->paginatorFactory ??= new PaginatorFactory(); } - private function createPaginator(bool $forceDisabledFetchJoinCollection = false): Paginator + public function setPaginatorFactory(PaginatorFactoryInterface $paginatorFactory): void { - $rootEntity = current($this->queryBuilder->getRootEntities()); - - if (false === $rootEntity) { - throw new \RuntimeException('There are no root entities defined in the query.'); - } - - $identifierFieldNames = $this->queryBuilder - ->getEntityManager() - ->getClassMetadata($rootEntity) - ->getIdentifierFieldNames(); - - $hasSingleIdentifierName = 1 === \count($identifierFieldNames); - $hasJoins = \count($this->queryBuilder->getDQLPart('join')) > 0; - - $query = (clone $this->queryBuilder)->getQuery(); - - $this->applyQueryHints($query); - - $query->setHydrationMode($this->hydrationMode); + $this->paginatorFactory = $paginatorFactory; + } - $fetchJoinCollection = $hasSingleIdentifierName && $hasJoins; + public function getAliasResolver(): AliasResolverInterface + { + return $this->aliasResolver ??= new AliasResolver(); + } - if ($forceDisabledFetchJoinCollection) { - $fetchJoinCollection = false; - } + public function setAliasResolver(AliasResolverInterface $aliasResolver): void + { + $this->aliasResolver = $aliasResolver; + } - return new Paginator($query, $fetchJoinCollection); + public function getResultSetFactory(): DoctrineOrmResultSetFactoryInterface + { + return $this->resultSetFactory ??= new DoctrineOrmResultSetFactory(); } - private function applyQueryHints(Query $query): void + public function setResultSetFactory(DoctrineOrmResultSetFactoryInterface $resultSetFactory): void { - foreach ($this->hints as $name => $value) { - $query->setHint($name, $value); - } + $this->resultSetFactory = $resultSetFactory; } } diff --git a/src/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryFactory.php b/src/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryFactory.php old mode 100755 new mode 100644 index 513399bf..1dfb94aa --- a/src/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryFactory.php +++ b/src/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryFactory.php @@ -7,10 +7,11 @@ 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): DoctrineOrmProxyQueryInterface + public function create(mixed $data): ProxyQueryInterface { 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 index c797b37d..708e9f0c 100644 --- a/src/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryInterface.php +++ b/src/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryInterface.php @@ -6,6 +6,7 @@ use Doctrine\ORM\QueryBuilder; use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Paginator\PaginatorFactoryInterface; /** * @mixin QueryBuilder @@ -16,18 +17,36 @@ public function getQueryBuilder(): QueryBuilder; public function getUniqueParameterId(): int; + /** + * @return array + */ + public function getHints(): array; + public function setHint(string $name, mixed $value): void; + /** + * @psalm-return string|AbstractQuery::HYDRATE_* + */ + public function getHydrationMode(): int|string; + /** * @psalm-param string|AbstractQuery::HYDRATE_* $hydrationMode */ public function setHydrationMode(int|string $hydrationMode): void; - public function isEntityManagerClearingEnabled(): bool; - - public function setEntityManagerClearingEnabled(bool $entityManagerClearingEnabled): void; - public function getBatchSize(): int; public function setBatchSize(int $batchSize): void; + + public function getPaginatorFactory(): PaginatorFactoryInterface; + + public function setPaginatorFactory(PaginatorFactoryInterface $paginatorFactory): void; + + public function getAliasResolver(): AliasResolverInterface; + + public function setAliasResolver(AliasResolverInterface $aliasResolver): void; + + public function getResultSetFactory(): DoctrineOrmResultSetFactoryInterface; + + public function setResultSetFactory(DoctrineOrmResultSetFactoryInterface $resultSetFactory): void; } diff --git a/src/Bridge/Doctrine/Orm/Query/DoctrineOrmResultSetFactory.php b/src/Bridge/Doctrine/Orm/Query/DoctrineOrmResultSetFactory.php new file mode 100644 index 00000000..e48200c2 --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Query/DoctrineOrmResultSetFactory.php @@ -0,0 +1,62 @@ + $this->getPaginatorItems($paginator, $batchSize)); + + $currentPageItemCount = $totalItemCount = $paginator->count(); + + if ($paginator->getQuery()->getMaxResults() > 0) { + $items = new \ArrayIterator(iterator_to_array($items)); + $currentPageItemCount = iterator_count($items); + } + + return new ResultSet($items, $currentPageItemCount, $totalItemCount); + } + + private function getPaginatorItems(Paginator $paginator, int $batchSize): \Generator + { + $query = $paginator->getQuery(); + + $firstResult = $query->getFirstResult(); + $maxResults = $limit = $query->getMaxResults(); + + if (null === $maxResults || $maxResults > $batchSize) { + $maxResults = $batchSize; + } + + $hasItems = true; + + $cursorPosition = 0; + + while ($hasItems && $firstResult < $paginator->count() && (null === $limit || $cursorPosition < $limit)) { + $hasItems = false; + + $query + ->setMaxResults($maxResults) + ->setFirstResult($firstResult); + + foreach ($paginator as $item) { + yield $item; + + $hasItems = true; + + ++$cursorPosition; + } + + $firstResult = $cursorPosition; + + $paginator->getQuery()->getEntityManager()->clear(); + } + } +} diff --git a/src/Bridge/Doctrine/Orm/Query/DoctrineOrmResultSetFactoryInterface.php b/src/Bridge/Doctrine/Orm/Query/DoctrineOrmResultSetFactoryInterface.php new file mode 100644 index 00000000..28d08084 --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Query/DoctrineOrmResultSetFactoryInterface.php @@ -0,0 +1,13 @@ +assertEquals($outputExpression, $expressionTransformer->transform($inputExpression)); + } +} diff --git a/src/Bridge/OpenSpout/Exporter/Type/AbstractOpenSpoutExporterType.php b/src/Bridge/OpenSpout/Exporter/Type/AbstractOpenSpoutExporterType.php index a8e89372..1a417707 100644 --- a/src/Bridge/OpenSpout/Exporter/Type/AbstractOpenSpoutExporterType.php +++ b/src/Bridge/OpenSpout/Exporter/Type/AbstractOpenSpoutExporterType.php @@ -78,7 +78,7 @@ private function getHeaderRowCells(HeaderRowView $view, array $options): array protected function getHeaderCell(ColumnHeaderView $view, array $options): Cell { return Cell::fromValue( - value: $view->vars['label'], + value: $view->vars['label'] ?? 'a', style: $this->getStyle($view, 'header_cell_style', $options), ); } @@ -94,7 +94,7 @@ protected function getValueRowCells(ValueRowView $view, array $options): array protected function getValueCell(ColumnValueView $view, array $options): Cell { return Cell::fromValue( - value: $view->value, + value: $view->vars['value'] ?? 'a', style: $this->getStyle($view, 'value_cell_style', $options), ); } diff --git a/src/Column/ColumnFactoryBuilder.php b/src/Column/ColumnFactoryBuilder.php index 1cd4b72c..c8ca5c41 100644 --- a/src/Column/ColumnFactoryBuilder.php +++ b/src/Column/ColumnFactoryBuilder.php @@ -6,7 +6,6 @@ use Kreyu\Bundle\DataTableBundle\Column\Extension\ColumnExtensionInterface; use Kreyu\Bundle\DataTableBundle\Column\Extension\ColumnTypeExtensionInterface; -use Kreyu\Bundle\DataTableBundle\Column\Extension\CoreColumnExtension; use Kreyu\Bundle\DataTableBundle\Column\Extension\PreloadedColumnExtension; use Kreyu\Bundle\DataTableBundle\Column\Type\ColumnTypeInterface; use Kreyu\Bundle\DataTableBundle\Column\Type\ResolvedColumnTypeFactory; @@ -31,11 +30,6 @@ class ColumnFactoryBuilder implements ColumnFactoryBuilderInterface */ private array $typeExtensions = []; - public function __construct( - private readonly bool $forceCoreExtension = false, - ) { - } - public function setResolvedTypeFactory(ResolvedColumnTypeFactoryInterface $resolvedTypeFactory): static { $this->resolvedTypeFactory = $resolvedTypeFactory; @@ -95,21 +89,6 @@ 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); } diff --git a/src/Column/ColumnRegistryInterface.php b/src/Column/ColumnRegistryInterface.php index 49a39c62..9ebd75b9 100755 --- a/src/Column/ColumnRegistryInterface.php +++ b/src/Column/ColumnRegistryInterface.php @@ -4,6 +4,7 @@ namespace Kreyu\Bundle\DataTableBundle\Column; +use Kreyu\Bundle\DataTableBundle\Column\Extension\ColumnExtensionInterface; use Kreyu\Bundle\DataTableBundle\Column\Type\ColumnTypeInterface; use Kreyu\Bundle\DataTableBundle\Column\Type\ResolvedColumnTypeInterface; @@ -13,4 +14,9 @@ interface ColumnRegistryInterface * @param class-string $name */ public function getType(string $name): ResolvedColumnTypeInterface; + + /** + * @return iterable + */ + public function getExtensions(): iterable; } diff --git a/src/Column/ColumnSortUrlGenerator.php b/src/Column/ColumnSortUrlGenerator.php new file mode 100644 index 00000000..f0558af2 --- /dev/null +++ b/src/Column/ColumnSortUrlGenerator.php @@ -0,0 +1,67 @@ +getRequest(); + + $route = $request->attributes->get('_route'); + $routeParams = $request->attributes->get('_route_params', []); + $queryParams = $request->query->all(); + + $parameters = [...$routeParams, ...$queryParams]; + + foreach ($columnHeaderViews as $columnHeaderView) { + $parameters = array_replace_recursive($parameters, $this->getColumnSortQueryParameters($columnHeaderView)); + } + + return $this->urlGenerator->generate($route, $parameters); + } + + private function getColumnSortQueryParameters(ColumnHeaderView $columnHeaderView): array + { + $dataTableView = $columnHeaderView->parent->parent; + + return [ + $dataTableView->vars['sort_parameter_name'] => [ + $columnHeaderView->vars['name'] => $this->getOppositeSortDirection($columnHeaderView), + ], + ]; + } + + private function getOppositeSortDirection(ColumnHeaderView $columnHeaderView): string + { + $sortDirection = mb_strtolower((string) $columnHeaderView->vars['sort_direction']); + + if ('asc' === $sortDirection) { + return 'desc'; + } + + return 'asc'; + } + + private function getRequest(): Request + { + if (null === $request = $this->requestStack->getCurrentRequest()) { + throw new LogicException('Unable to retrieve current request.'); + } + + return $request; + } +} diff --git a/src/Column/ColumnSortUrlGeneratorInterface.php b/src/Column/ColumnSortUrlGeneratorInterface.php new file mode 100644 index 00000000..ee55b77b --- /dev/null +++ b/src/Column/ColumnSortUrlGeneratorInterface.php @@ -0,0 +1,10 @@ + $types - * @param array> $typeExtensions + * @param array $types + * @param array|array> $typeExtensions */ public function __construct( private readonly array $types = [], diff --git a/src/DataTable.php b/src/DataTable.php index cb7dbc9c..c779f50d 100755 --- a/src/DataTable.php +++ b/src/DataTable.php @@ -28,6 +28,8 @@ 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\CurrentPageOutOfRangeException; +use Kreyu\Bundle\DataTableBundle\Pagination\Pagination; use Kreyu\Bundle\DataTableBundle\Pagination\PaginationData; use Kreyu\Bundle\DataTableBundle\Pagination\PaginationInterface; use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceAdapterInterface; @@ -36,38 +38,39 @@ use Kreyu\Bundle\DataTableBundle\Personalization\Form\Type\PersonalizationDataType; use Kreyu\Bundle\DataTableBundle\Personalization\PersonalizationData; use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface; +use Kreyu\Bundle\DataTableBundle\Query\ResultSetInterface; use Kreyu\Bundle\DataTableBundle\Sorting\SortingData; use Symfony\Component\Form\FormBuilderInterface; class DataTable implements DataTableInterface { /** - * @var array + * @var array */ private array $columns = []; /** - * @var array + * @var array */ private array $filters = []; /** - * @var array + * @var array */ private array $actions = []; /** - * @var array + * @var array */ private array $batchActions = []; /** - * @var array + * @var array */ private array $rowActions = []; /** - * @var array + * @var array */ private array $exporters = []; @@ -106,6 +109,8 @@ class DataTable implements DataTableInterface */ private ProxyQueryInterface $originalQuery; + private ?ResultSetInterface $resultSet = null; + private bool $initialized = false; public function __construct( @@ -165,7 +170,7 @@ public function getColumns(): array { $columns = $this->columns; - uasort($columns, static function (ColumnInterface $columnA, ColumnInterface $columnB) { + uasort($columns, static function (ColumnInterface $columnA, ColumnInterface $columnB): int { return $columnB->getPriority() <=> $columnA->getPriority(); }); @@ -517,7 +522,7 @@ public function filter(FiltrationData $data, bool $persistence = true): void $filterData = $data->getFilterData($filter->getName()); if ($filterData && $filterData->hasValue()) { - $filter->apply($this->query, $filterData); + $filter->handle($this->query, $filterData); } } @@ -593,11 +598,7 @@ public function export(ExportData $data = null): ExportFile public function getItems(): iterable { - if ($this->getConfig()->isPaginationEnabled()) { - return $this->getPagination()->getItems(); - } - - return $this->query->getItems(); + return $this->getResultSet()->getIterator(); } public function getPagination(): PaginationInterface @@ -606,7 +607,33 @@ public function getPagination(): PaginationInterface throw new RuntimeException('The data table has pagination feature disabled.'); } - return $this->pagination ??= $this->query->getPagination(); + return $this->pagination ??= $this->createPagination(); + } + + private function getResultSet(): ResultSetInterface + { + return $this->resultSet ??= $this->query->getResult(); + } + + private function createPagination(): PaginationInterface + { + $resultSet = $this->getResultSet(); + + try { + return new Pagination( + currentPageNumber: $this->paginationData?->getPage() ?? 1, + currentPageItemCount: $resultSet->getCurrentPageItemCount(), + totalItemCount: $resultSet->getTotalItemCount(), + itemNumberPerPage: $this->paginationData?->getPerPage(), + ); + } catch (CurrentPageOutOfRangeException) { + $this->paginationData ??= new PaginationData(); + $this->paginationData->setPage(1); + + $this->resultSet = null; + + return $this->createPagination(); + } } public function getSortingData(): ?SortingData @@ -675,11 +702,7 @@ public function createFiltrationFormBuilder(DataTableView $view = null): FormBui throw new RuntimeException('The data table has filtration feature disabled.'); } - if (null === $formFactory = $this->config->getFiltrationFormFactory()) { - throw new RuntimeException('The data table has no configured filtration form factory.'); - } - - return $formFactory->createNamedBuilder( + return $this->config->getFiltrationFormFactory()->createNamedBuilder( name: $this->config->getFiltrationParameterName(), type: FiltrationDataType::class, options: [ diff --git a/src/DataTableConfigBuilder.php b/src/DataTableConfigBuilder.php index 1a574b56..5f63e18b 100755 --- a/src/DataTableConfigBuilder.php +++ b/src/DataTableConfigBuilder.php @@ -46,7 +46,7 @@ class DataTableConfigBuilder implements DataTableConfigBuilderInterface private ?PersistenceSubjectProviderInterface $filtrationPersistenceSubjectProvider = null; private ?PersistenceSubjectProviderInterface $personalizationPersistenceSubjectProvider = null; - private ?FormFactoryInterface $filtrationFormFactory = null; + private FormFactoryInterface $filtrationFormFactory; private ?FormFactoryInterface $personalizationFormFactory = null; private ?FormFactoryInterface $exportFormFactory = null; @@ -456,8 +456,12 @@ public function setFiltrationPersistenceSubjectProvider(?PersistenceSubjectProvi return $this; } - public function getFiltrationFormFactory(): ?FormFactoryInterface + public function getFiltrationFormFactory(): FormFactoryInterface { + if (!isset($this->filtrationFormFactory)) { + throw new BadMethodCallException('The filtration form factory must be set before retrieving it.'); + } + return $this->filtrationFormFactory; } diff --git a/src/DataTableConfigBuilderInterface.php b/src/DataTableConfigBuilderInterface.php index 0cf609ee..41a8cb32 100755 --- a/src/DataTableConfigBuilderInterface.php +++ b/src/DataTableConfigBuilderInterface.php @@ -77,7 +77,7 @@ public function setFiltrationPersistenceAdapter(?PersistenceAdapterInterface $fi public function setFiltrationPersistenceSubjectProvider(?PersistenceSubjectProviderInterface $filtrationPersistenceSubjectProvider): static; - public function setFiltrationFormFactory(?FormFactoryInterface $filtrationFormFactory): static; + public function setFiltrationFormFactory(FormFactoryInterface $filtrationFormFactory): static; public function setDefaultFiltrationData(?FiltrationData $defaultFiltrationData): static; diff --git a/src/DataTableConfigInterface.php b/src/DataTableConfigInterface.php index 76b79703..ed3f092b 100755 --- a/src/DataTableConfigInterface.php +++ b/src/DataTableConfigInterface.php @@ -77,7 +77,7 @@ public function getFiltrationPersistenceAdapter(): ?PersistenceAdapterInterface; public function getFiltrationPersistenceSubjectProvider(): ?PersistenceSubjectProviderInterface; - public function getFiltrationFormFactory(): ?FormFactoryInterface; + public function getFiltrationFormFactory(): FormFactoryInterface; public function getDefaultFiltrationData(): ?FiltrationData; diff --git a/src/DataTableRegistryInterface.php b/src/DataTableRegistryInterface.php index 0243ee5d..473fb515 100755 --- a/src/DataTableRegistryInterface.php +++ b/src/DataTableRegistryInterface.php @@ -4,9 +4,19 @@ namespace Kreyu\Bundle\DataTableBundle; +use Kreyu\Bundle\DataTableBundle\Extension\DataTableExtensionInterface; +use Kreyu\Bundle\DataTableBundle\Type\DataTableTypeInterface; use Kreyu\Bundle\DataTableBundle\Type\ResolvedDataTableTypeInterface; interface DataTableRegistryInterface { + /** + * @param class-string $name + */ public function getType(string $name): ResolvedDataTableTypeInterface; + + /** + * @return iterable + */ + public function getExtensions(): iterable; } diff --git a/src/DependencyInjection/KreyuDataTableExtension.php b/src/DependencyInjection/KreyuDataTableExtension.php index 5b479d27..6e58139b 100755 --- a/src/DependencyInjection/KreyuDataTableExtension.php +++ b/src/DependencyInjection/KreyuDataTableExtension.php @@ -97,7 +97,7 @@ public function prepend(ContainerBuilder $container): void $container->prependExtensionConfig('framework', [ 'asset_mapper' => [ 'paths' => [ - __DIR__ . '/../../assets/controllers' => '@kreyu/data-table-bundle', + __DIR__.'/../../assets/controllers' => '@kreyu/data-table-bundle', ], ], ]); @@ -141,6 +141,6 @@ private function isAssetMapperAvailable(ContainerBuilder $container): bool return false; } - return is_file($bundlesMetadata['FrameworkBundle']['path'] . '/Resources/config/asset_mapper.php'); + return is_file($bundlesMetadata['FrameworkBundle']['path'].'/Resources/config/asset_mapper.php'); } } diff --git a/src/Exporter/ExporterRegistryInterface.php b/src/Exporter/ExporterRegistryInterface.php index 10c7cc2e..dc714593 100755 --- a/src/Exporter/ExporterRegistryInterface.php +++ b/src/Exporter/ExporterRegistryInterface.php @@ -4,6 +4,7 @@ namespace Kreyu\Bundle\DataTableBundle\Exporter; +use Kreyu\Bundle\DataTableBundle\Exporter\Extension\ExporterExtensionInterface; use Kreyu\Bundle\DataTableBundle\Exporter\Type\ExporterTypeInterface; use Kreyu\Bundle\DataTableBundle\Exporter\Type\ResolvedExporterTypeInterface; @@ -13,4 +14,9 @@ interface ExporterRegistryInterface * @param class-string $name */ public function getType(string $name): ResolvedExporterTypeInterface; + + /** + * @return iterable + */ + public function getExtensions(): iterable; } diff --git a/src/Exporter/Extension/PreloadedExporterExtension.php b/src/Exporter/Extension/PreloadedExporterExtension.php index f553c335..2170dffe 100644 --- a/src/Exporter/Extension/PreloadedExporterExtension.php +++ b/src/Exporter/Extension/PreloadedExporterExtension.php @@ -4,14 +4,13 @@ namespace Kreyu\Bundle\DataTableBundle\Exporter\Extension; -use Kreyu\Bundle\DataTableBundle\Column\Extension\AbstractColumnExtension; -use Kreyu\Bundle\DataTableBundle\Column\Type\ColumnTypeInterface; +use Kreyu\Bundle\DataTableBundle\Exporter\Type\ExporterTypeInterface; -class PreloadedExporterExtension extends AbstractColumnExtension +class PreloadedExporterExtension extends AbstractExporterExtension { /** - * @param array $types - * @param array> $typeExtensions + * @param array $types + * @param array|array> $typeExtensions */ public function __construct( private readonly array $types = [], diff --git a/src/Extension/PreloadedDataTableExtension.php b/src/Extension/PreloadedDataTableExtension.php index b281506a..11df279e 100644 --- a/src/Extension/PreloadedDataTableExtension.php +++ b/src/Extension/PreloadedDataTableExtension.php @@ -9,8 +9,8 @@ class PreloadedDataTableExtension extends AbstractDataTableExtension { /** - * @param array $types - * @param array> $typeExtensions + * @param array $types + * @param array|array> $typeExtensions */ public function __construct( private readonly array $types = [], diff --git a/src/Filter/Event/FilterEvent.php b/src/Filter/Event/FilterEvent.php new file mode 100644 index 00000000..8f3f4035 --- /dev/null +++ b/src/Filter/Event/FilterEvent.php @@ -0,0 +1,40 @@ +query; + } + + public function getData(): FilterData + { + return $this->data; + } + + public function setData(FilterData $data): void + { + $this->data = $data; + } + + public function getFilter(): FilterInterface + { + return $this->filter; + } +} diff --git a/src/Filter/Event/FilterEvents.php b/src/Filter/Event/FilterEvents.php new file mode 100644 index 00000000..5805827a --- /dev/null +++ b/src/Filter/Event/FilterEvents.php @@ -0,0 +1,16 @@ + $types - * @param array> $typeExtensions + * @param array $types + * @param array|array> $typeExtensions */ public function __construct( private readonly array $types = [], diff --git a/src/Filter/Filter.php b/src/Filter/Filter.php index e1b3d3cd..daf49e02 100755 --- a/src/Filter/Filter.php +++ b/src/Filter/Filter.php @@ -7,11 +7,15 @@ use Kreyu\Bundle\DataTableBundle\DataTableInterface; use Kreyu\Bundle\DataTableBundle\DataTableView; use Kreyu\Bundle\DataTableBundle\Exception\BadMethodCallException; +use Kreyu\Bundle\DataTableBundle\Filter\Event\FilterEvent; +use Kreyu\Bundle\DataTableBundle\Filter\Event\FilterEvents; +use Kreyu\Bundle\DataTableBundle\Filter\Event\PostHandleEvent; +use Kreyu\Bundle\DataTableBundle\Filter\Event\PreHandleEvent; use Kreyu\Bundle\DataTableBundle\Query\ProxyQueryInterface; class Filter implements FilterInterface { - private ?DataTableInterface $dataTable = null; + private DataTableInterface $dataTable; public function __construct( private readonly FilterConfigInterface $config, @@ -30,7 +34,7 @@ public function getConfig(): FilterConfigInterface public function getDataTable(): DataTableInterface { - if (null === $this->dataTable) { + if (!isset($this->dataTable)) { throw new BadMethodCallException('Filter is not attached to any data table.'); } @@ -53,7 +57,7 @@ public function getFormOptions(): array { return [ 'form_type' => $this->config->getFormType(), - 'form_options' => $this->config->getFormOptions(), + 'form_options' => $this->config->getFormOptions() + ['required' => false], 'operator_form_type' => $this->config->getOperatorFormType(), 'operator_form_options' => $this->config->getOperatorFormOptions(), 'default_operator' => $this->config->getDefaultOperator(), @@ -67,16 +71,39 @@ public function getQueryPath(): string return $this->config->getOption('query_path', $this->getName()); } - public function apply(ProxyQueryInterface $query = null, FilterData $data = null): void + public function handle(ProxyQueryInterface $query, FilterData $data): void { - $query ??= $this->getDataTable()->getQuery(); + $this->dispatch(FilterEvents::PRE_HANDLE, $event = new PreHandleEvent($query, $data, $this)); - 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.'; + $this->config->getHandler()->handle($query, $data = $event->getData(), $this); - throw new BadMethodCallException($error); + $this->dispatch(FilterEvents::POST_HANDLE, new PostHandleEvent($query, $data, $this)); + } + + public function createView(FilterData $data, DataTableView $parent): FilterView + { + $view = $this->config->getType()->createView($this, $data, $parent); + + $this->config->getType()->buildView($view, $this, $data, $this->config->getOptions()); + + return $view; + } + + private function dispatch(string $eventName, FilterEvent $event): void + { + $dispatcher = $this->config->getEventDispatcher(); + + if ($dispatcher->hasListeners($eventName)) { + $dispatcher->dispatch($event, $eventName); } + } + + /** + * @deprecated since 0.15, use {@see Filter::handle()} instead + */ + public function apply(ProxyQueryInterface $query = null, FilterData $data = null): void + { + $query ??= $this->getDataTable()->getQuery(); $data ??= $this->getDataTable()->getFiltrationData()->getFilterData($this); @@ -87,15 +114,14 @@ public function apply(ProxyQueryInterface $query = null, FilterData $data = null throw new BadMethodCallException($error); } - $this->config->getType()->apply($query, $data, $this, $this->config->getOptions()); - } + $type = $this->config->getType(); - public function createView(FilterData $data, DataTableView $parent): FilterView - { - $view = $this->config->getType()->createView($this, $data, $parent); + if (method_exists($type, 'apply')) { + $type->apply($query, $data, $this, $this->config->getOptions()); - $this->config->getType()->buildView($view, $this, $data, $this->config->getOptions()); + return; + } - return $view; + $this->handle($query, $data); } } diff --git a/src/Filter/FilterClearUrlGenerator.php b/src/Filter/FilterClearUrlGenerator.php new file mode 100644 index 00000000..437e8d2c --- /dev/null +++ b/src/Filter/FilterClearUrlGenerator.php @@ -0,0 +1,59 @@ +getRequest(); + + $route = $request->attributes->get('_route'); + $routeParams = $request->attributes->get('_route_params', []); + $queryParams = $request->query->all(); + + $parameters = [...$routeParams, ...$queryParams]; + + foreach ($filterViews as $filterView) { + $parameters = array_replace_recursive($parameters, $this->getFilterClearQueryParameters($filterView)); + } + + return $this->urlGenerator->generate($route, $parameters); + } + + private function getFilterClearQueryParameters(FilterView $filterView): array + { + $dataTableView = $filterView->parent; + + return [ + $dataTableView->vars['filtration_parameter_name'] => [ + $filterView->vars['name'] => [ + 'value' => '', + 'operator' => null, + ], + ], + ]; + } + + private function getRequest(): Request + { + if (null === $request = $this->requestStack->getCurrentRequest()) { + throw new LogicException('Unable to retrieve current request.'); + } + + return $request; + } +} diff --git a/src/Filter/FilterClearUrlGeneratorInterface.php b/src/Filter/FilterClearUrlGeneratorInterface.php new file mode 100644 index 00000000..2338421e --- /dev/null +++ b/src/Filter/FilterClearUrlGeneratorInterface.php @@ -0,0 +1,10 @@ +name; + $this->dispatcher->addListener($eventName, $listener, $priority); + + return $this; } - public function setName(string $name): static + public function addEventSubscriber(EventSubscriberInterface $subscriber): static { - if ($this->locked) { - throw $this->createBuilderLockedException(); + $this->dispatcher->addSubscriber($subscriber); + + return $this; + } + + public function getEventDispatcher(): EventDispatcherInterface + { + if (!$this->dispatcher instanceof ImmutableEventDispatcher) { + $this->dispatcher = new ImmutableEventDispatcher($this->dispatcher); } - $this->name = $name; + return $this->dispatcher; + } - return $this; + public function getName(): string + { + return $this->name; } public function getType(): ResolvedFilterTypeInterface @@ -75,24 +93,22 @@ public function getOption(string $name, mixed $default = null): mixed return $this->options[$name] ?? $default; } - public function setOptions(array $options): static + public function getHandler(): FilterHandlerInterface { - if ($this->locked) { - throw $this->createBuilderLockedException(); + if (!isset($this->handler)) { + throw new BadMethodCallException('Filter has no handler set'); } - $this->options = $options; - - return $this; + return $this->handler; } - public function setOption(string $name, mixed $value): static + public function setHandler(FilterHandlerInterface $handler): static { if ($this->locked) { throw $this->createBuilderLockedException(); } - $this->options[$name] = $value; + $this->handler = $handler; return $this; } @@ -179,8 +195,7 @@ public function setSupportedOperators(array $supportedOperators): static public function getDefaultOperator(): Operator { - // TODO: Remove "getNonDeprecatedCase()" call once the deprecated operators are removed. - return $this->defaultOperator->getNonDeprecatedCase(); + return $this->defaultOperator; } public function setDefaultOperator(Operator $defaultOperator): static @@ -210,6 +225,22 @@ public function setOperatorSelectable(bool $operatorSelectable): static return $this; } + public function getEmptyData(): FilterData + { + return $this->emptyData ??= new FilterData(); + } + + public function setEmptyData(FilterData $emptyData): static + { + if ($this->locked) { + throw $this->createBuilderLockedException(); + } + + $this->emptyData = $emptyData; + + return $this; + } + public function getFilterConfig(): FilterConfigInterface { if ($this->locked) { diff --git a/src/Filter/FilterConfigBuilderInterface.php b/src/Filter/FilterConfigBuilderInterface.php index 12827adf..130fd52a 100755 --- a/src/Filter/FilterConfigBuilderInterface.php +++ b/src/Filter/FilterConfigBuilderInterface.php @@ -5,26 +5,18 @@ namespace Kreyu\Bundle\DataTableBundle\Filter; use Kreyu\Bundle\DataTableBundle\Filter\Type\ResolvedFilterTypeInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Form\FormTypeInterface; interface FilterConfigBuilderInterface extends FilterConfigInterface { - /** - * @deprecated since 0.14.0, provide the name using the factory {@see FilterFactoryInterface} "named" methods instead - */ - public function setName(string $name): static; + public function addEventListener(string $eventName, callable $listener, int $priority = 0): static; - public function setType(ResolvedFilterTypeInterface $type): static; + public function addEventSubscriber(EventSubscriberInterface $subscriber): static; - /** - * @deprecated since 0.14.0, modifying the options dynamically will be removed as it creates unexpected behaviors - */ - public function setOptions(array $options): static; + public function setType(ResolvedFilterTypeInterface $type): 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 setHandler(FilterHandlerInterface $handler): static; /** * @param class-string $formType diff --git a/src/Filter/FilterConfigInterface.php b/src/Filter/FilterConfigInterface.php index c74f983e..7b69699c 100755 --- a/src/Filter/FilterConfigInterface.php +++ b/src/Filter/FilterConfigInterface.php @@ -5,9 +5,13 @@ namespace Kreyu\Bundle\DataTableBundle\Filter; use Kreyu\Bundle\DataTableBundle\Filter\Type\ResolvedFilterTypeInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Form\FormTypeInterface; interface FilterConfigInterface { + public function getEventDispatcher(): EventDispatcherInterface; + public function getName(): string; public function getType(): ResolvedFilterTypeInterface; @@ -18,6 +22,22 @@ public function hasOption(string $name): bool; public function getOption(string $name, mixed $default = null): mixed; + public function getHandler(): FilterHandlerInterface; + + /** + * @return class-string + */ + public function getFormType(): string; + + public function getFormOptions(): array; + + /** + * @return class-string + */ + public function getOperatorFormType(): string; + + public function getOperatorFormOptions(): array; + /** * @return array */ diff --git a/src/Filter/FilterData.php b/src/Filter/FilterData.php index f3e2ed1a..f220d5d1 100755 --- a/src/Filter/FilterData.php +++ b/src/Filter/FilterData.php @@ -10,7 +10,7 @@ class FilterData { public function __construct( - private mixed $value = '', + private mixed $value = null, private ?Operator $operator = null, ) { } @@ -24,9 +24,9 @@ public static function fromArray(array $data = []): self ]) ->setAllowedTypes('operator', ['null', 'string', Operator::class]) ->setNormalizer('operator', function (Options $options, mixed $value): ?Operator { - // TODO: Remove call to "getNonDeprecatedCase()" - return is_string($value) ? Operator::from($value)->getNonDeprecatedCase() : $value; + return is_string($value) ? Operator::from($value) : $value; }) + ->setIgnoreUndefined() ; $data = $resolver->resolve($data); @@ -37,28 +37,28 @@ public static function fromArray(array $data = []): self ); } - public function getOperator(): ?Operator + public function getValue(): mixed { - return $this->operator; + return $this->value; } - public function setOperator(mixed $operator): void + public function setValue(mixed $value): void { - $this->operator = $operator; + $this->value = $value; } - public function getValue(): mixed + public function getOperator(): ?Operator { - return $this->value; + return $this->operator; } - public function setValue(mixed $value): void + public function setOperator(mixed $operator): void { - $this->value = $value; + $this->operator = $operator; } public function hasValue(): bool { - return '' !== $this->value; + return null !== $this->value && '' !== $this->value; } } diff --git a/src/Filter/FilterHandlerInterface.php b/src/Filter/FilterHandlerInterface.php new file mode 100644 index 00000000..9d590d07 --- /dev/null +++ b/src/Filter/FilterHandlerInterface.php @@ -0,0 +1,12 @@ + $name */ public function getType(string $name): ResolvedFilterTypeInterface; + + /** + * @return iterable + */ + public function getExtensions(): iterable; } diff --git a/src/Filter/FiltrationData.php b/src/Filter/FiltrationData.php index dc77bf8d..f9e98b6b 100755 --- a/src/Filter/FiltrationData.php +++ b/src/Filter/FiltrationData.php @@ -6,7 +6,7 @@ use Kreyu\Bundle\DataTableBundle\DataTableInterface; -class FiltrationData +class FiltrationData implements \ArrayAccess { /** * @var array filter name as key, filter data as value @@ -49,10 +49,6 @@ public static function fromArray(array $data): self return new self($filters); } - /** - * Creates a new instance from a {@see DataTableInterface}. - * The filters are be initialized with empty values. - */ public static function fromDataTable(DataTableInterface $dataTable): self { $filters = []; @@ -119,11 +115,11 @@ public function removeFilter(string|FilterInterface $filter): void /** * @param array $filters */ - public function appendMissingFilters(array $filters, FilterData $data = new FilterData()): void + public function appendMissingFilters(array $filters): void { foreach ($filters as $filter) { if (null === $this->getFilterData($filter)) { - $this->setFilterData($filter, $data); + $this->setFilterData($filter, new FilterData(operator: $filter->getConfig()->getDefaultOperator())); } } } @@ -153,4 +149,24 @@ public function isEmpty(): bool { return empty($this->filters); } + + public function offsetExists(mixed $offset): bool + { + return array_key_exists($offset, $this->filters); + } + + public function offsetGet(mixed $offset): FilterData + { + return $this->filters[$offset]; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + $this->setFilterData($offset, $value); + } + + public function offsetUnset(mixed $offset): void + { + $this->removeFilter($offset); + } } diff --git a/src/Filter/Form/Type/FilterDataType.php b/src/Filter/Form/Type/FilterDataType.php index efc626ad..530be0f1 100755 --- a/src/Filter/Form/Type/FilterDataType.php +++ b/src/Filter/Form/Type/FilterDataType.php @@ -7,27 +7,37 @@ use Kreyu\Bundle\DataTableBundle\Filter\FilterData; use Kreyu\Bundle\DataTableBundle\Filter\Operator; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\DataMapperInterface; +use Symfony\Component\Form\CallbackTransformer; 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\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; -class FilterDataType extends AbstractType implements DataMapperInterface +class FilterDataType extends AbstractType { - 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('value', $options['form_type'], $options['form_options']) - ->add('operator', $options['operator_form_type'], $options['operator_form_options']) - ->setDataMapper($this) - ; + $builder->add('value', $options['form_type'], $options['form_options']); + + $builder->get('value')->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { + if ('' === $event->getData()) { + $event->setData(null); + } + }); + + if ($options['operator_selectable']) { + $builder->add('operator', $options['operator_form_type'], $options['operator_form_options'] + [ + 'empty_data' => $options['default_operator'], + 'choices' => $options['supported_operators'], + ]); + + $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 @@ -40,17 +50,20 @@ public function configureOptions(OptionsResolver $resolver): void 'form_options' => [], 'operator_form_type' => OperatorType::class, 'operator_form_options' => [], - 'default_operator' => Operator::Equals, 'supported_operators' => [], 'operator_selectable' => false, + 'default_operator' => Operator::Equals, + 'empty_data' => function (Options $options) { + return new FilterData(operator: $options['default_operator']); + }, ]) ->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.'[]') + ->setAllowedTypes('default_operator', Operator::class) ; } @@ -58,36 +71,4 @@ public function getBlockPrefix(): string { return 'kreyu_data_table_filter_data'; } - - public function mapDataToForms(mixed $viewData, \Traversable $forms): void - { - if (!$viewData instanceof FilterData) { - return; - } - - $forms = iterator_to_array($forms); - $forms['value']->setData($viewData->hasValue() ? $viewData->getValue() : null); - $forms['operator']->setData($viewData->getOperator()); - } - - public function mapFormsToData(\Traversable $forms, mixed &$viewData): void - { - 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 index 65660d86..d4a94515 100755 --- a/src/Filter/Form/Type/FiltrationDataType.php +++ b/src/Filter/Form/Type/FiltrationDataType.php @@ -7,17 +7,15 @@ use Kreyu\Bundle\DataTableBundle\DataTableInterface; use Kreyu\Bundle\DataTableBundle\DataTableView; use Kreyu\Bundle\DataTableBundle\Filter\FilterData; -use Kreyu\Bundle\DataTableBundle\Filter\FilterInterface; use Kreyu\Bundle\DataTableBundle\Filter\FiltrationData; use Kreyu\Bundle\DataTableBundle\Filter\Type\SearchFilterTypeInterface; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\DataMapperInterface; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; use Symfony\Component\OptionsResolver\OptionsResolver; -class FiltrationDataType extends AbstractType implements DataMapperInterface +class FiltrationDataType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { @@ -27,18 +25,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $dataTable = $options['data_table']; 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); - }, - ])); + $builder->add($filter->getFormName(), FilterDataType::class, $filter->getFormOptions() + [ + 'getter' => fn (FiltrationData $filtrationData) => $filtrationData->getFilterData($filter), + 'setter' => fn (FiltrationData $filtrationData, FilterData $filterData) => $filtrationData->setFilterData($filter, $filterData), + ]); } - - $builder->setDataMapper($this); } public function finishView(FormView $view, FormInterface $form, array $options): void @@ -75,10 +66,7 @@ public function finishView(FormView $view, FormInterface $form, array $options): } if ($filter->getConfig()->getType()->getInnerType() instanceof SearchFilterTypeInterface) { - $searchField = $view[$child->getName()]; - $searchField->vars['attr']['form'] = $view->vars['id']; - - $searchFields[] = $searchField; + $searchFields[] = $view[$child->getName()]; unset($view[$child->getName()]); } @@ -95,37 +83,13 @@ public function configureOptions(OptionsResolver $resolver): void 'data_class' => FiltrationData::class, 'csrf_protection' => false, 'data_table_view' => null, - 'filters' => [], ]) ->setRequired('data_table') ->setAllowedTypes('data_table', DataTableInterface::class) ->setAllowedTypes('data_table_view', ['null', DataTableView::class]) - ->setAllowedTypes('filters', FilterInterface::class.'[]') ; } - public function mapDataToForms(mixed $viewData, \Traversable $forms): void - { - if (null === $viewData) { - return; - } - - foreach (iterator_to_array($forms) as $name => $form) { - $form->setData($viewData->getFilterData($name)); - } - } - - public function mapFormsToData(\Traversable $forms, mixed &$viewData): void - { - if (null === $viewData) { - $viewData = new FiltrationData(); - } - - foreach (iterator_to_array($forms) as $name => $form) { - $viewData->setFilterData($name, $form->getData()); - } - } - private function applyFormAttributeRecursively(FormView $view, string $id): void { $view->vars['attr']['form'] = $id; diff --git a/src/Filter/Operator.php b/src/Filter/Operator.php index 0b4bacff..d0bc74ba 100755 --- a/src/Filter/Operator.php +++ b/src/Filter/Operator.php @@ -10,99 +10,32 @@ enum Operator: string case NotEquals = 'not-equals'; case Contains = 'contains'; case NotContains = 'not-contains'; + case In = 'in'; + case NotIn = 'not-in'; 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'; + case Between = 'between'; 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, + self::Equals => 'Equals', + self::NotContains => 'Not contains', + self::Contains => 'Contains', + self::NotEquals => 'Not equals', + self::In => 'In', + self::NotIn => 'Not in', + self::GreaterThan => 'Greater than', + self::GreaterThanEquals => 'Greater than or equals', + self::LessThan => 'Less than', + self::LessThanEquals => 'Less than or equals', + self::StartsWith => 'Starts with', + self::EndsWith => 'Ends with', + self::Between => 'Between', }; } } diff --git a/src/Filter/Type/CallbackFilterType.php b/src/Filter/Type/CallbackFilterType.php new file mode 100644 index 00000000..d7cd4a9c --- /dev/null +++ b/src/Filter/Type/CallbackFilterType.php @@ -0,0 +1,33 @@ +setHandler($this); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setRequired('callback') + ->setAllowedTypes('callback', 'callable') + ; + } + + public function handle(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter): void + { + $filter->getConfig()->getOption('callback')($query, $data, $filter); + } +} diff --git a/src/Filter/Type/FilterType.php b/src/Filter/Type/FilterType.php index cf7121f3..46b8b2ba 100755 --- a/src/Filter/Type/FilterType.php +++ b/src/Filter/Type/FilterType.php @@ -10,20 +10,13 @@ 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\Contracts\Translation\TranslatableInterface; final class FilterType implements FilterTypeInterface { - public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter, array $options): void - { - } - public function buildFilter(FilterBuilderInterface $builder, array $options): void { $setters = [ @@ -43,35 +36,34 @@ public function buildFilter(FilterBuilderInterface $builder, array $options): vo public function buildView(FilterView $view, FilterInterface $filter, FilterData $data, array $options): void { - $value = $data; + $value = null; + + if ($data->hasValue()) { + $value = $data->getValue(); - if ($value->hasValue() && $formatter = $options['active_filter_formatter']) { - $value = $formatter($data, $filter, $options); + if ($formatter = $options['active_filter_formatter']) { + $value = $formatter($data, $filter, $options); + } } $view->data = $data; $view->value = $value; $view->vars = array_replace($view->vars, [ - 'name' => $options['name'] ?? $filter->getName(), - 'form_name' => $options['form_name'] ?? $filter->getFormName(), + 'name' => $filter->getName(), + 'query_path' => $filter->getQueryPath(), 'label' => $options['label'] ?? StringUtil::camelToSentence($filter->getName()), 'label_translation_parameters' => $options['label_translation_parameters'], - 'translation_domain' => $options['translation_domain'] ?? $view->parent->vars['translation_domain'], - 'query_path' => $options['query_path'] ?? $filter->getName(), - 'field_Type' => $options['field_type'] ?? $options['form_type'], - 'field_type' => $options['field_type'] ?? $options['form_type'], - 'field_options' => $options['field_options'] ?? $options['form_options'], - 'operator_type' => $options['operator_type'] ?? $options['operator_form_type'], - 'operator_options' => $options['operator_options'] ?? $options['operator_form_options'], - 'active_filter_formatter' => $options['active_filter_formatter'], + 'translation_domain' => $options['translation_domain'] ?? $view->parent->vars['translation_domain'] ?? null, + 'form_type' => $filter->getConfig()->getFormType(), + 'form_options' => $filter->getConfig()->getFormOptions(), + 'operator_form_type' => $filter->getConfig()->getOperatorFormType(), + 'operator_form_options' => $filter->getConfig()->getOperatorFormOptions(), + 'operator_selectable' => $filter->getConfig()->isOperatorSelectable(), + 'default_operator' => $filter->getConfig()->getDefaultOperator(), + 'supported_operators' => $filter->getConfig()->getSupportedOperators(), 'data' => $view->data, 'value' => $view->value, - 'operator_selectable' => $filter->getConfig()->isOperatorSelectable(), - 'clear_filter_parameters' => [ - 'value' => $options['empty_data'], - 'operator' => $filter->getConfig()->getDefaultOperator()->value, - ], ]); } @@ -90,121 +82,22 @@ public function configureOptions(OptionsResolver $resolver): void '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' => null, -// 'operator_options' => [ -// 'visible' => null, -// 'choices' => [], -// ], - ]) - ->setDefined([ - 'auto_alias_resolving', - 'field_type', - 'field_options', - 'operator_type', - 'operator_options', + 'active_filter_formatter' => null, ]) - - ->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('label_translation_parameters', 'array') + ->setAllowedTypes('translation_domain', ['null', 'bool', 'string']) ->setAllowedTypes('query_path', ['null', 'string']) - ->setAllowedTypes('form_type', ['string']) - ->setAllowedTypes('form_options', ['array']) - ->setAllowedTypes('operator_form_type', ['string']) - ->setAllowedTypes('operator_form_options', ['array']) + ->setAllowedTypes('form_type', 'string') + ->setAllowedTypes('form_options', 'array') + ->setAllowedTypes('operator_form_type', 'string') + ->setAllowedTypes('operator_form_options', 'array') + ->setAllowedTypes('default_operator', Operator::class) + ->setAllowedTypes('supported_operators', Operator::class.'[]') + ->setAllowedTypes('operator_selectable', 'bool') ->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', ['null', 'string']) - ->setAllowedTypes('operator_options', ['array']) - ->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) { - if (!$options->offsetExists('field_type')) { - return $value; - } - - return $options['field_type'] ?? $value; - }) - ->addNormalizer('form_options', function (Options $options, mixed $value) { - if (!$options->offsetExists('field_options')) { - return $value; - } - - return $options['field_options'] ?: $value; - }) - ->addNormalizer('operator_form_type', function (Options $options, mixed $value) { - if (!$options->offsetExists('operator_type')) { - return $value; - } - - return $options['operator_type'] ?? $value; - }) - ->addNormalizer('operator_form_options', function (Options $options, mixed $value) { - if (!$options->offsetExists('operator_options')) { - return $value; - } - - if ($deprecatedValue = $options['operator_options']) { - unset($deprecatedValue['visible'], $deprecatedValue['choices']); - } - - return $deprecatedValue ?: $value; - }) - ->addNormalizer('supported_operators', function (Options $options, mixed $value) { - if (!$options->offsetExists('operator_options')) { - return $value; - } - - return ($options['operator_options']['choices'] ?? []) ?: $value; - }) - ->addNormalizer('default_operator', function (Options $options, mixed $value) { - if (!$options->offsetExists('operator_options')) { - return $value; - } - - $deprecatedChoices = $options['operator_options']['choices'] ?? []; - - return reset($deprecatedChoices) ?: $value; - }) - ->addNormalizer('operator_selectable', function (Options $options, mixed $value) { - if (!$options->offsetExists('operator_options')) { - return $value; - } - - return ($options['operator_options']['visible'] ?? null) ?: $value; + ->setAllowedValues('translation_domain', function (mixed $value): bool { + return is_null($value) || false === $value || is_string($value); }) ; } diff --git a/src/Filter/Type/FilterTypeInterface.php b/src/Filter/Type/FilterTypeInterface.php index 9803007a..94260fc4 100755 --- a/src/Filter/Type/FilterTypeInterface.php +++ b/src/Filter/Type/FilterTypeInterface.php @@ -8,13 +8,10 @@ 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; interface FilterTypeInterface { - 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; diff --git a/src/Filter/Type/ResolvedFilterType.php b/src/Filter/Type/ResolvedFilterType.php index d2b99db0..1ca94938 100755 --- a/src/Filter/Type/ResolvedFilterType.php +++ b/src/Filter/Type/ResolvedFilterType.php @@ -12,7 +12,7 @@ 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\EventDispatcher\EventDispatcher; use Symfony\Component\OptionsResolver\Exception\ExceptionInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -24,9 +24,9 @@ class ResolvedFilterType implements ResolvedFilterTypeInterface * @param array $typeExtensions */ public function __construct( - private FilterTypeInterface $innerType, - private array $typeExtensions = [], - private ?ResolvedFilterTypeInterface $parent = null, + private readonly FilterTypeInterface $innerType, + private readonly array $typeExtensions = [], + private readonly ?ResolvedFilterTypeInterface $parent = null, ) { } @@ -61,7 +61,7 @@ public function createBuilder(FilterFactoryInterface $factory, string $name, arr 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); + return new FilterBuilder($name, $this, new EventDispatcher(), $options); } public function createView(FilterInterface $filter, FilterData $data, DataTableView $parent): FilterView @@ -91,17 +91,6 @@ public function buildView(FilterView $view, FilterInterface $filter, FilterData } } - public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter, array $options): void - { - $this->parent?->apply($query, $data, $filter, $options); - - $this->innerType->apply($query, $data, $filter, $options); - - foreach ($this->typeExtensions as $extension) { - $extension->apply($query, $data, $filter, $options); - } - } - public function getOptionsResolver(): OptionsResolver { if (!isset($this->optionsResolver)) { diff --git a/src/Filter/Type/ResolvedFilterTypeInterface.php b/src/Filter/Type/ResolvedFilterTypeInterface.php index 69e8b26a..7b9920dd 100755 --- a/src/Filter/Type/ResolvedFilterTypeInterface.php +++ b/src/Filter/Type/ResolvedFilterTypeInterface.php @@ -11,7 +11,6 @@ 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\OptionsResolver; interface ResolvedFilterTypeInterface @@ -35,7 +34,5 @@ public function buildFilter(FilterBuilderInterface $builder, array $options): vo public function buildView(FilterView $view, FilterInterface $filter, FilterData $data, array $options): void; - public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter, array $options): void; - public function getOptionsResolver(): OptionsResolver; } diff --git a/src/Filter/Type/SearchFilterType.php b/src/Filter/Type/SearchFilterType.php index 77714e4f..3eff1041 100755 --- a/src/Filter/Type/SearchFilterType.php +++ b/src/Filter/Type/SearchFilterType.php @@ -4,23 +4,20 @@ namespace Kreyu\Bundle\DataTableBundle\Filter\Type; -use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Query\DoctrineOrmProxyQuery; +use Kreyu\Bundle\DataTableBundle\Filter\FilterBuilderInterface; use Kreyu\Bundle\DataTableBundle\Filter\FilterData; +use Kreyu\Bundle\DataTableBundle\Filter\FilterHandlerInterface; 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 +class SearchFilterType extends AbstractFilterType implements SearchFilterTypeInterface, FilterHandlerInterface { - /** - * @param DoctrineOrmProxyQuery $query - */ - public function apply(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter, array $options): void + public function buildFilter(FilterBuilderInterface $builder, array $options): void { - $options['handler']($query, (string) $data->getValue(), $filter); + $builder->setHandler($this); } public function configureOptions(OptionsResolver $resolver): void @@ -29,7 +26,6 @@ public function configureOptions(OptionsResolver $resolver): void ->setDefaults([ 'form_type' => SearchType::class, 'label' => false, - 'supported_operators' => Operator::cases(), ]) ->setRequired('handler') ->setAllowedTypes('handler', 'callable') @@ -40,4 +36,9 @@ public function configureOptions(OptionsResolver $resolver): void }) ; } + + public function handle(ProxyQueryInterface $query, FilterData $data, FilterInterface $filter): void + { + $filter->getConfig()->getOption('handler')($query, (string) $data->getValue(), $filter); + } } diff --git a/src/Pagination/Pagination.php b/src/Pagination/Pagination.php index aa7a5a3c..c5699300 100755 --- a/src/Pagination/Pagination.php +++ b/src/Pagination/Pagination.php @@ -12,21 +12,16 @@ class Pagination implements PaginationInterface * @throws CurrentPageOutOfRangeException */ public function __construct( - private iterable $items, - private int $currentPageNumber, - private int $totalItemCount, - private ?int $itemNumberPerPage = null, + private readonly int $currentPageNumber, + private readonly int $currentPageItemCount, + private readonly int $totalItemCount, + private readonly ?int $itemNumberPerPage = null, ) { if ($totalItemCount > 0 && $this->isCurrentPageNumberOutOfRange()) { throw new CurrentPageOutOfRangeException(); } } - public function getItems(): iterable - { - return $this->items; - } - public function getCurrentPageNumber(): int { return $this->currentPageNumber; @@ -34,11 +29,7 @@ public function getCurrentPageNumber(): int public function getCurrentPageItemCount(): int { - if ($this->items instanceof \Traversable) { - return iterator_count($this->items); - } - - return count((array) $this->items); + return $this->currentPageItemCount; } public function getTotalItemCount(): int diff --git a/src/Pagination/PaginationInterface.php b/src/Pagination/PaginationInterface.php index 0cf2e36a..8343ddb0 100755 --- a/src/Pagination/PaginationInterface.php +++ b/src/Pagination/PaginationInterface.php @@ -9,11 +9,6 @@ interface PaginationInterface public const DEFAULT_PAGE = 1; public const DEFAULT_PER_PAGE = 25; - /** - * @return iterable - */ - public function getItems(): iterable; - public function getCurrentPageNumber(): int; public function getCurrentPageItemCount(): int; diff --git a/src/Persistence/CachePersistenceAdapter.php b/src/Persistence/CachePersistenceAdapter.php index cf3f158f..2144c2c7 100755 --- a/src/Persistence/CachePersistenceAdapter.php +++ b/src/Persistence/CachePersistenceAdapter.php @@ -51,13 +51,11 @@ public function read(DataTableInterface $dataTable, PersistenceSubjectInterface private function getCacheKey(DataTableInterface $dataTable, PersistenceSubjectInterface $subject): string { - $parts = [ - $dataTable->getConfig()->getName(), + return urlencode(implode('_', array_filter([ + $dataTable->getName(), $this->prefix, $subject->getDataTablePersistenceIdentifier(), - ]; - - return u(implode('_', array_filter($parts)))->snake()->toString(); + ]))); } /** diff --git a/src/Personalization/PersonalizationColumnData.php b/src/Personalization/PersonalizationColumnData.php index 7cdb78c5..60905ff1 100755 --- a/src/Personalization/PersonalizationColumnData.php +++ b/src/Personalization/PersonalizationColumnData.php @@ -23,7 +23,7 @@ public function __construct( */ public static function fromArray(array $data): self { - $resolver = static::$optionsResolver ??= (new OptionsResolver()) + $resolver = self::$optionsResolver ??= (new OptionsResolver()) ->setRequired('name') ->setDefaults([ 'priority' => 0, diff --git a/src/Personalization/PersonalizationData.php b/src/Personalization/PersonalizationData.php index 86bca8ba..9b54524b 100755 --- a/src/Personalization/PersonalizationData.php +++ b/src/Personalization/PersonalizationData.php @@ -28,7 +28,7 @@ public function __construct(array $columns = []) public static function fromArray(array $data): self { - $resolver = static::$optionsResolver ??= (new OptionsResolver()) + $resolver = self::$optionsResolver ??= (new OptionsResolver()) ->setDefaults([ 'columns' => function (OptionsResolver $resolver) { $resolver @@ -38,7 +38,6 @@ public static function fromArray(array $data): self 'priority' => 0, 'visible' => true, ]) - ->setDeprecated('order') ->setAllowedTypes('name', ['null', 'string']) ->setAllowedTypes('priority', 'int') ->setAllowedTypes('visible', 'bool') diff --git a/src/Query/ProxyQueryInterface.php b/src/Query/ProxyQueryInterface.php index baaa1b0c..1ac02deb 100755 --- a/src/Query/ProxyQueryInterface.php +++ b/src/Query/ProxyQueryInterface.php @@ -5,7 +5,6 @@ namespace Kreyu\Bundle\DataTableBundle\Query; use Kreyu\Bundle\DataTableBundle\Pagination\PaginationData; -use Kreyu\Bundle\DataTableBundle\Pagination\PaginationInterface; use Kreyu\Bundle\DataTableBundle\Sorting\SortingData; interface ProxyQueryInterface @@ -14,7 +13,5 @@ public function sort(SortingData $sortingData): void; public function paginate(PaginationData $paginationData): void; - public function getPagination(): PaginationInterface; - - public function getItems(): iterable; + public function getResult(): ResultSetInterface; } diff --git a/src/Query/ResultSet.php b/src/Query/ResultSet.php new file mode 100644 index 00000000..db953997 --- /dev/null +++ b/src/Query/ResultSet.php @@ -0,0 +1,35 @@ +iterator; + } + + public function getCurrentPageItemCount(): ?int + { + return $this->currentPageItemCount; + } + + public function getTotalItemCount(): ?int + { + return $this->totalItemCount ??= $this->currentPageItemCount; + } + + public function count(): int + { + return $this->getCurrentPageItemCount() ?? $this->getTotalItemCount(); + } +} diff --git a/src/Query/ResultSetInterface.php b/src/Query/ResultSetInterface.php new file mode 100644 index 00000000..e453df96 --- /dev/null +++ b/src/Query/ResultSetInterface.php @@ -0,0 +1,12 @@ +set('kreyu_data_table.column.type.text', TextColumnType::class) ->tag('kreyu_data_table.column.type') ; + + $services + ->set('kreyu_data_table.column.column_sort_url_generator', ColumnSortUrlGenerator::class) + ->args([ + service('request_stack'), + service(UrlGeneratorInterface::class), + ]) + ->alias(ColumnSortUrlGeneratorInterface::class, 'kreyu_data_table.column.column_sort_url_generator') + ; }; diff --git a/src/Resources/config/filtration.php b/src/Resources/config/filtration.php index fdb54661..78f126f9 100755 --- a/src/Resources/config/filtration.php +++ b/src/Resources/config/filtration.php @@ -3,7 +3,7 @@ declare(strict_types=1); use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\BooleanFilterType; -use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\CallbackFilterType; +use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\CallbackFilterType as DoctrineOrmCallbackFilterType; 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; @@ -11,16 +11,20 @@ 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; +use Kreyu\Bundle\DataTableBundle\Filter\FilterClearUrlGenerator; +use Kreyu\Bundle\DataTableBundle\Filter\FilterClearUrlGeneratorInterface; use Kreyu\Bundle\DataTableBundle\Filter\FilterFactory; use Kreyu\Bundle\DataTableBundle\Filter\FilterFactoryInterface; use Kreyu\Bundle\DataTableBundle\Filter\FilterRegistry; use Kreyu\Bundle\DataTableBundle\Filter\FilterRegistryInterface; +use Kreyu\Bundle\DataTableBundle\Filter\Type\CallbackFilterType; use Kreyu\Bundle\DataTableBundle\Filter\Type\FilterType; use Kreyu\Bundle\DataTableBundle\Filter\Type\ResolvedFilterTypeFactory; use Kreyu\Bundle\DataTableBundle\Filter\Type\ResolvedFilterTypeFactoryInterface; use Kreyu\Bundle\DataTableBundle\Filter\Type\SearchFilterType; use Kreyu\Bundle\DataTableBundle\Persistence\CachePersistenceAdapter; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use function Symfony\Component\DependencyInjection\Loader\Configurator\service; use function Symfony\Component\DependencyInjection\Loader\Configurator\tagged_iterator; @@ -72,6 +76,20 @@ ->tag('kreyu_data_table.filter.type') ; + $services + ->set('kreyu_data_table.filter.type.callback', CallbackFilterType::class) + ->tag('kreyu_data_table.filter.type') + ; + + $services + ->set('kreyu_data_table.filter.filter_clear_url_generator', FilterClearUrlGenerator::class) + ->args([ + service('request_stack'), + service(UrlGeneratorInterface::class), + ]) + ->alias(FilterClearUrlGeneratorInterface::class, 'kreyu_data_table.filter.filter_clear_url_generator') + ; + // Doctrine ORM $services @@ -96,7 +114,7 @@ ; $services - ->set('kreyu_data_table.filter.type.doctrine_orm_callback', CallbackFilterType::class) + ->set('kreyu_data_table.filter.type.doctrine_orm_callback', DoctrineOrmCallbackFilterType::class) ->tag('kreyu_data_table.filter.type') ; diff --git a/src/Resources/config/twig.php b/src/Resources/config/twig.php index 6690bcaf..e6512217 100755 --- a/src/Resources/config/twig.php +++ b/src/Resources/config/twig.php @@ -5,11 +5,17 @@ use Kreyu\Bundle\DataTableBundle\Twig\DataTableExtension; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use function Symfony\Component\DependencyInjection\Loader\Configurator\service; + return static function (ContainerConfigurator $configurator) { $services = $configurator->services(); $services ->set('kreyu_data_table.twig.data_table_extension', DataTableExtension::class) ->tag('twig.extension') + ->args([ + service('kreyu_data_table.column.column_sort_url_generator'), + service('kreyu_data_table.filter.filter_clear_url_generator'), + ]) ; }; diff --git a/src/Resources/translations/KreyuDataTable.pl.yaml b/src/Resources/translations/KreyuDataTable.pl.yaml index 49372132..f5a638c6 100755 --- a/src/Resources/translations/KreyuDataTable.pl.yaml +++ b/src/Resources/translations/KreyuDataTable.pl.yaml @@ -3,12 +3,15 @@ Equals: równa się Not equals: różne od Contains: zawiera Not contains: nie zawiera +In: zawiera się w +Not in: nie zawiera się w 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 +Between: między # Boolean type Yes: Tak diff --git a/src/Resources/views/macros.html.twig b/src/Resources/views/macros.html.twig deleted file mode 100755 index e318d193..00000000 --- a/src/Resources/views/macros.html.twig +++ /dev/null @@ -1,27 +0,0 @@ -{% macro filter_clear_url(filtration_parameter_name, filter) %} - {{ path( - 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): filter.vars.clear_filter_parameters - }) - }) - ) }} -{% endmacro %} - -{% macro filter_clear_all_url(filtration_parameter_name, filters) %} - {% set query = app.request.query.all(filtration_parameter_name) %} - - {% for filter in filters %} - {% set query = query|merge({ - (filter.vars.name): filter.vars.clear_filter_parameters - }) %} - {% endfor %} - - {{ path( - app.request.get('_route'), - app.request.attributes.get('_route_params')|merge(app.request.query.all)|merge({ - (filtration_parameter_name): query - }) - ) }} -{% endmacro %} diff --git a/src/Resources/views/themes/base.html.twig b/src/Resources/views/themes/base.html.twig index 8679a02e..16bc3073 100755 --- a/src/Resources/views/themes/base.html.twig +++ b/src/Resources/views/themes/base.html.twig @@ -179,9 +179,9 @@ {% block pagination_counters_message %} {{- 'Showing %current_page_first_item_index% - %current_page_last_item_index% of %total_item_count%'|trans({ - '%current_page_first_item_index%': current_page_first_item_index, - '%current_page_last_item_index%': current_page_last_item_index, - '%total_item_count%': total_item_count + '%current_page_first_item_index%': current_page_first_item_index|number_format(0, '', ' '), + '%current_page_last_item_index%': current_page_last_item_index|number_format(0, '', ' '), + '%total_item_count%': total_item_count|number_format(0, '', ' ') }, 'KreyuDataTable') -}} {% endblock %} diff --git a/src/Resources/views/themes/bootstrap_5.html.twig b/src/Resources/views/themes/bootstrap_5.html.twig index 791af3d5..073d02d5 100755 --- a/src/Resources/views/themes/bootstrap_5.html.twig +++ b/src/Resources/views/themes/bootstrap_5.html.twig @@ -194,11 +194,9 @@ {% endblock %} {% block filter_clear_all_button %} - {% from '@KreyuDataTable/macros.html.twig' import filter_clear_all_url %} - {% set attr = { 'class': 'btn btn-icon btn-outline-danger', - 'href': filter_clear_all_url(filtration_parameter_name, filters)|trim|raw, + 'href': data_table_filter_clear_url(filters), 'data-toggle': 'tooltip', 'data-placement': 'bottom', 'title': 'Clear all filters'|trans({}, 'KreyuDataTable'), @@ -209,10 +207,8 @@ {% endblock %} {% block filter_clear_button %} - {% from '@KreyuDataTable/macros.html.twig' import filter_clear_url %} - {{ filter.vars.label|trans({}, filter.vars.translation_domain) }} {% if filter.vars.operator_selectable %} - ({{ filter.vars.data.operator.label|trans({}, 'KreyuDataTable') }}) + {{ filter.vars.data.operator.label|trans({}, 'KreyuDataTable') }} {% endif %} @@ -540,9 +536,11 @@ {{ form_label(form) }}
-
+
+ {% if form.operator is defined %} + {{ form_widget(form.operator) }} + {% endif %} {{ form_widget(form.value) }} - {{ form_widget(form.operator) }}
diff --git a/src/Sorting/SortingData.php b/src/Sorting/SortingData.php index fa195e34..bf92afa2 100755 --- a/src/Sorting/SortingData.php +++ b/src/Sorting/SortingData.php @@ -37,6 +37,7 @@ public static function fromArray(array $data): self $fields[$key] = SortingColumnData::fromArray([ 'name' => $key, 'direction' => $value, + 'property_path' => null, ]); } } diff --git a/src/Test/Exporter/ExporterIntegrationTestCase.php b/src/Test/Exporter/ExporterIntegrationTestCase.php new file mode 100644 index 00000000..f92d256c --- /dev/null +++ b/src/Test/Exporter/ExporterIntegrationTestCase.php @@ -0,0 +1,38 @@ +factory = DataTables::createExporterFactoryBuilder() + ->addExtensions($this->getExtensions()) + ->addTypeExtensions($this->getTypeExtensions()) + ->addTypes($this->getTypes()) + ->getExporterFactory(); + } + + protected function getExtensions(): array + { + return []; + } + + protected function getTypeExtensions(): array + { + return []; + } + + protected function getTypes(): array + { + return []; + } +} diff --git a/src/Test/Exporter/ExporterTypeTestCase.php b/src/Test/Exporter/ExporterTypeTestCase.php new file mode 100644 index 00000000..0331eda9 --- /dev/null +++ b/src/Test/Exporter/ExporterTypeTestCase.php @@ -0,0 +1,59 @@ + + */ + abstract protected function getTestedType(): string; + + protected function createExporter(array $options = []): ExporterInterface + { + return $this->factory->create($this->getTestedType(), $options); + } + + protected function createNamedExporter(string $name, array $options = []): ExporterInterface + { + return $this->factory->createNamed($name, $this->getTestedType(), $options); + } + + protected function createDataTableView(array $headers = [], array $valueRows = []): DataTableView + { + $view = new DataTableView(); + $view->headerRow = new HeaderRowView($view); + + foreach ($headers as $label) { + $headerRow = new ColumnHeaderView($view->headerRow); + $headerRow->vars['label'] = $label; + + $view->headerRow->children[] = $headerRow; + } + + foreach ($valueRows as $index => $data) { + $valueRow = new ValueRowView($view, $index, $data); + + foreach ($data as $value) { + $valueView = new ColumnValueView($valueRow); + $valueView->data = $valueView->value = $value; + + $valueRow->children[] = $valueView; + } + + $view->valueRows[] = $valueRow; + } + + return $view; + } +} diff --git a/src/Test/Filter/FilterIntegrationTestCase.php b/src/Test/Filter/FilterIntegrationTestCase.php new file mode 100644 index 00000000..ff1159c1 --- /dev/null +++ b/src/Test/Filter/FilterIntegrationTestCase.php @@ -0,0 +1,38 @@ +factory = DataTables::createFilterFactoryBuilder() + ->addExtensions($this->getExtensions()) + ->addTypeExtensions($this->getTypeExtensions()) + ->addTypes($this->getTypes()) + ->getFilterFactory(); + } + + protected function getExtensions(): array + { + return []; + } + + protected function getTypeExtensions(): array + { + return []; + } + + protected function getTypes(): array + { + return []; + } +} diff --git a/src/Test/Filter/FilterTypeTestCase.php b/src/Test/Filter/FilterTypeTestCase.php new file mode 100644 index 00000000..eaff9ba0 --- /dev/null +++ b/src/Test/Filter/FilterTypeTestCase.php @@ -0,0 +1,48 @@ + + */ + abstract protected function getTestedType(): string; + + protected function createFilter(array $options = []): FilterInterface + { + return $this->factory->create($this->getTestedType(), $options); + } + + protected function createNamedFilter(string $name, array $options = []): FilterInterface + { + return $this->factory->createNamed($name, $this->getTestedType(), $options); + } + + protected function createFilterView(FilterInterface $filter, FilterData $data = null, DataTableView $parent = null): FilterView + { + return $filter->createView( + data: $data ?? $this->createFilterDataMock(), + parent: $parent ?? $this->createDataTableViewMock(), + ); + } + + protected function createFilterDataMock(): MockObject&FilterData + { + return $this->createMock(FilterData::class); + } + + protected function createDataTableViewMock(): MockObject&DataTableView + { + return $this->createMock(DataTableView::class); + } +} diff --git a/src/Twig/DataTableExtension.php b/src/Twig/DataTableExtension.php index 7f9ba878..3fdc3931 100755 --- a/src/Twig/DataTableExtension.php +++ b/src/Twig/DataTableExtension.php @@ -6,8 +6,11 @@ use Kreyu\Bundle\DataTableBundle\Action\ActionView; use Kreyu\Bundle\DataTableBundle\Column\ColumnHeaderView; +use Kreyu\Bundle\DataTableBundle\Column\ColumnSortUrlGeneratorInterface; use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView; use Kreyu\Bundle\DataTableBundle\DataTableView; +use Kreyu\Bundle\DataTableBundle\Filter\FilterClearUrlGeneratorInterface; +use Kreyu\Bundle\DataTableBundle\Filter\FilterView; use Kreyu\Bundle\DataTableBundle\HeaderRowView; use Kreyu\Bundle\DataTableBundle\Pagination\PaginationView; use Kreyu\Bundle\DataTableBundle\ValueRowView; @@ -21,6 +24,12 @@ class DataTableExtension extends AbstractExtension { + public function __construct( + private readonly ColumnSortUrlGeneratorInterface $columnSortUrlGenerator, + private readonly FilterClearUrlGeneratorInterface $filterClearUrlGenerator, + ) { + } + public function getFunctions(): array { $definitions = [ @@ -40,7 +49,10 @@ public function getFunctions(): array 'data_table_export_form' => $this->renderExportForm(...), ]; - $functions = []; + $functions = [ + new TwigFunction('data_table_filter_clear_url', $this->generateFilterClearUrl(...)), + new TwigFunction('data_table_column_sort_url', $this->generateColumnSortUrl(...)), + ]; foreach ($definitions as $name => $callable) { $functions[] = new TwigFunction($name, $callable, [ @@ -279,6 +291,24 @@ public function renderExportForm(Environment $environment, FormInterface|FormVie ); } + public function generateFilterClearUrl(FilterView|array $filterViews): string + { + if ($filterViews instanceof FilterView) { + $filterViews = [$filterViews]; + } + + return $this->filterClearUrlGenerator->generate(...$filterViews); + } + + public function generateColumnSortUrl(ColumnHeaderView|array $columnHeaderViews): string + { + if ($columnHeaderViews instanceof ColumnHeaderView) { + $columnHeaderViews = [$columnHeaderViews]; + } + + return $this->columnSortUrlGenerator->generate(...$columnHeaderViews); + } + /** * @param array $context * diff --git a/src/Type/DataTableType.php b/src/Type/DataTableType.php index 19ee0966..b56f7ff3 100755 --- a/src/Type/DataTableType.php +++ b/src/Type/DataTableType.php @@ -154,12 +154,12 @@ public function buildView(DataTableView $view, DataTableInterface $dataTable, ar public function buildExportView(DataTableView $view, DataTableInterface $dataTable, array $options): void { - $visibleColumns = $dataTable->getExportableColumns(); - $view->vars['translation_domain'] = $dataTable->getConfig()->getOption('translation_domain'); - $view->headerRow = $this->createExportHeaderRowView($view, $dataTable, $visibleColumns); - $view->valueRows = new RowIterator(fn () => $this->createExportValueRowsViews($view, $dataTable, $visibleColumns)); + $columns = $dataTable->getExportableColumns(); + + $view->headerRow = $this->createExportHeaderRowView($view, $dataTable, $columns); + $view->valueRows = new RowIterator(fn () => $this->createExportValueRowsViews($view, $dataTable, $columns)); } public function configureOptions(OptionsResolver $resolver): void @@ -315,6 +315,8 @@ private function createHeaderRowView(DataTableView $view, DataTableInterface $da $headerRowView->children[$column->getName()] = $column->createHeaderView($headerRowView); } + $headerRowView->vars['children'] = $headerRowView->children; + return $headerRowView; } @@ -325,11 +327,7 @@ private function createHeaderRowView(DataTableView $view, DataTableInterface $da */ private function createValueRowsViews(DataTableView $view, DataTableInterface $dataTable, array $columns): iterable { - if ($dataTable->getConfig()->isPaginationEnabled()) { - $items = $dataTable->getPagination()->getItems(); - } else { - $items = $dataTable->getQuery()->getItems(); - } + $items = $dataTable->getItems(); foreach ($items as $index => $data) { $valueRowView = new ValueRowView($view, $index, $data); @@ -369,13 +367,12 @@ private function createExportHeaderRowView(DataTableView $view, DataTableInterfa return $headerRowView; } + /** + * @param array $columns + */ private function createExportValueRowsViews(DataTableView $view, DataTableInterface $dataTable, array $columns): iterable { - if ($dataTable->getConfig()->isPaginationEnabled()) { - $items = $dataTable->getPagination()->getItems(); - } else { - $items = $dataTable->getQuery()->getItems(); - } + $items = $dataTable->getItems(); foreach ($items as $index => $data) { $valueRowView = new ValueRowView($view, $index, $data); diff --git a/src/Type/ResolvedDataTableType.php b/src/Type/ResolvedDataTableType.php index daf6b71e..63d38dc1 100755 --- a/src/Type/ResolvedDataTableType.php +++ b/src/Type/ResolvedDataTableType.php @@ -23,9 +23,9 @@ class ResolvedDataTableType implements ResolvedDataTableTypeInterface * @param array $typeExtensions */ public function __construct( - private DataTableTypeInterface $innerType, - private array $typeExtensions = [], - private ?ResolvedDataTableTypeInterface $parent = null, + private readonly DataTableTypeInterface $innerType, + private readonly array $typeExtensions = [], + private readonly ?ResolvedDataTableTypeInterface $parent = null, ) { } @@ -60,9 +60,7 @@ public function createBuilder(DataTableFactoryInterface $factory, string $name, throw new $exception(sprintf('An error has occurred resolving the options of the data table "%s": ', get_debug_type($this->getInnerType())).$exception->getMessage(), $exception->getCode(), $exception); } - $builder = new DataTableBuilder($name, $this, $query, new EventDispatcher(), $options); - - return $builder; + return new DataTableBuilder($name, $this, $query, new EventDispatcher(), $options); } public function createView(DataTableInterface $dataTable): DataTableView @@ -100,7 +98,7 @@ public function buildView(DataTableView $view, DataTableInterface $dataTable, ar public function buildExportView(DataTableView $view, DataTableInterface $dataTable, array $options): void { if ($this->parent && method_exists($this->parent, 'buildExportView')) { - $this->parent?->buildExportView($view, $dataTable, $options); + $this->parent->buildExportView($view, $dataTable, $options); } if (method_exists($this->innerType, 'buildExportView')) { diff --git a/src/Type/ResolvedDataTableTypeInterface.php b/src/Type/ResolvedDataTableTypeInterface.php index 79562345..e8b245da 100755 --- a/src/Type/ResolvedDataTableTypeInterface.php +++ b/src/Type/ResolvedDataTableTypeInterface.php @@ -32,6 +32,8 @@ public function createBuilder(DataTableFactoryInterface $factory, string $name, public function createView(DataTableInterface $dataTable): DataTableView; + public function createExportView(DataTableInterface $dataTable): DataTableView; + /** * @param array $options */ @@ -42,5 +44,10 @@ public function buildDataTable(DataTableBuilderInterface $builder, array $option */ public function buildView(DataTableView $view, DataTableInterface $dataTable, array $options): void; + /** + * @param array $options + */ + public function buildExportView(DataTableView $view, DataTableInterface $dataTable, array $options): void; + public function getOptionsResolver(): OptionsResolver; } diff --git a/src/Util/RewindableGeneratorIterator.php b/src/Util/RewindableGeneratorIterator.php new file mode 100644 index 00000000..07fe563a --- /dev/null +++ b/src/Util/RewindableGeneratorIterator.php @@ -0,0 +1,51 @@ +callable = $callable(...); + + $this->recreateGenerator(); + } + + private function recreateGenerator(): void + { + $this->generator = ($this->callable)(); + } + + public function current(): mixed + { + return $this->generator->current(); + } + + public function next(): void + { + $this->generator->next(); + } + + public function key(): mixed + { + return $this->generator->key(); + } + + public function valid(): bool + { + return $this->generator->valid(); + } + + public function rewind(): void + { + $this->recreateGenerator(); + } +} diff --git a/tests/Fixtures/Filter/CustomFilterExtension.php b/tests/Fixtures/Filter/CustomFilterExtension.php new file mode 100644 index 00000000..a4b3b8c7 --- /dev/null +++ b/tests/Fixtures/Filter/CustomFilterExtension.php @@ -0,0 +1,32 @@ +setDefined(['foo']); + } + + public function getBlockPrefix(): string + { + return 'custom_filter'; + } +} diff --git a/tests/Fixtures/Filter/CustomFilterTypeExtension.php b/tests/Fixtures/Filter/CustomFilterTypeExtension.php new file mode 100644 index 00000000..2893fed0 --- /dev/null +++ b/tests/Fixtures/Filter/CustomFilterTypeExtension.php @@ -0,0 +1,41 @@ +createMock(ProxyQueryInterface::class), + $this->createMock(FilterData::class), + $this->createMock(FilterInterface::class), + [], + ); + + $parameters = [new Parameter('foo', 'bar')]; + + $event->setParameters($parameters); + + $this->assertEquals($parameters, $event->getParameters()); + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Extension/DoctrineOrmFilterExtensionTest.php b/tests/Unit/Bridge/Doctrine/Orm/Extension/DoctrineOrmFilterExtensionTest.php new file mode 100644 index 00000000..1cbccf81 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Extension/DoctrineOrmFilterExtensionTest.php @@ -0,0 +1,40 @@ +assertTrue($extension->hasType($type)); + $this->assertInstanceOf($type, $extension->getType($type)); + } + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Filter/DoctrineOrmFilterHandlerTest.php b/tests/Unit/Bridge/Doctrine/Orm/Filter/DoctrineOrmFilterHandlerTest.php new file mode 100644 index 00000000..4648ceea --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Filter/DoctrineOrmFilterHandlerTest.php @@ -0,0 +1,164 @@ +createFilterMock(); + $filterConfig = $this->createFilterConfigMock(); + $eventDispatcher = $this->createEventDispatcherMock(); + + $filter->method('getConfig')->willReturn($filterConfig); + $filterConfig->method('getEventDispatcher')->willReturn($eventDispatcher); + + $this->filter = $filter; + $this->eventDispatcher = $eventDispatcher; + $this->query = $this->createDoctrineOrmProxyQueryMock(); + $this->data = $this->createFilterDataMock(); + } + + public function testItThrowsExceptionWithNotSupportedProxyQueryClass(): void + { + $query = new NotSupportedProxyQuery(); + + $this->expectExceptionObject(new UnexpectedTypeException($query, DoctrineOrmProxyQueryInterface::class)); + + $this->createHandler()->handle($query, $this->data, $this->filter); + } + + public function testItAppliesExpression(): void + { + ($queryBuilder = $this->createQueryBuilderMock()) + ->expects($this->once()) + ->method('andWhere') + ->willReturnCallback(function (mixed $expression) { + $this->assertEquals('expression', $expression); + }); + + $this->query->method('getQueryBuilder')->willReturn($queryBuilder); + + $this->createHandler(expression: 'expression')->handle($this->query, $this->data, $this->filter); + } + + public function testItSetsParameter(): void + { + ($queryBuilder = $this->createQueryBuilderMock()) + ->expects($this->once()) + ->method('setParameter') + ->willReturnCallback(function ($name, $value) { + $this->assertEquals('foo', $name); + $this->assertEquals('bar', $value); + }); + + $this->query->method('getQueryBuilder')->willReturn($queryBuilder); + + $this->createHandler(parameters: [new Parameter('foo', 'bar')])->handle($this->query, $this->data, $this->filter); + } + + public function testItDispatchesEvents(): void + { + $this->eventDispatcher + ->expects($matcher = $this->exactly(2)) + ->method('dispatch') + ->willReturnCallback(function (DoctrineOrmFilterEvent $event, string $eventName) use ($matcher) { + // @phpstan-ignore-next-line + $this->assertInstanceOf(match ($matcher->numberOfInvocations()) { + 1 => PreSetParametersEvent::class, + 2 => PreApplyExpressionEvent::class, + }, $event); + + // @phpstan-ignore-next-line + $this->assertEquals(match ($matcher->numberOfInvocations()) { + 1 => DoctrineOrmFilterEvents::PRE_SET_PARAMETERS, + 2 => DoctrineOrmFilterEvents::PRE_APPLY_EXPRESSION, + }, $eventName); + + $this->assertEquals($this->query, $event->getQuery()); + $this->assertEquals($this->data, $event->getData()); + $this->assertEquals($this->filter, $event->getFilter()); + + return $event; + }); + + $this->createHandler()->handle($this->query, $this->data, $this->filter); + } + + private function createHandler(mixed $expression = null, array $parameters = []): DoctrineOrmFilterHandler + { + $expressionFactory = $this->createMock(ExpressionFactoryInterface::class); + $expressionFactory->method('create')->willReturn($expression); + + $parameterFactory = $this->createMock(ParameterFactoryInterface::class); + $parameterFactory->method('create')->willReturn($parameters); + + return new DoctrineOrmFilterHandler($expressionFactory, $parameterFactory); + } + + private function createFilterMock(): MockObject&FilterInterface + { + return $this->createMock(FilterInterface::class); + } + + private function createFilterConfigMock(): FilterConfigInterface&MockObject + { + $filterConfig = $this->createMock(FilterConfigInterface::class); + $filterConfig->method('getSupportedOperators')->willReturn([Operator::Equals]); + + return $filterConfig; + } + + private function createFilterDataMock(): FilterData&MockObject + { + $filterData = $this->createMock(FilterData::class); + $filterData->method('getOperator')->willReturn(Operator::Equals); + + return $filterData; + } + + private function createDoctrineOrmProxyQueryMock(): MockObject&DoctrineOrmProxyQueryInterface + { + return $this->createMock(DoctrineOrmProxyQueryInterface::class); + } + + private function createQueryBuilderMock(): MockObject&QueryBuilder + { + return $this->createMock(QueryBuilder::class); + } + + private function createEventDispatcherMock(): MockObject&EventDispatcherInterface + { + $eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $eventDispatcher->method('hasListeners')->willReturn(true); + + return $eventDispatcher; + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Filter/EventListener/ApplyExpressionTransformersTest.php b/tests/Unit/Bridge/Doctrine/Orm/Filter/EventListener/ApplyExpressionTransformersTest.php new file mode 100644 index 00000000..202a94b6 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Filter/EventListener/ApplyExpressionTransformersTest.php @@ -0,0 +1,92 @@ +listener = new ApplyExpressionTransformers(); + } + + #[DataProvider('providePreApplyExpressionCases')] + public function testPreApplyExpression(array $options, string $expected) + { + $expression = new Expr\Comparison('foo', '=', 'bar'); + + $event = new PreApplyExpressionEvent( + $this->createMock(DoctrineOrmProxyQueryInterface::class), + $this->createMock(FilterData::class), + $this->createFilterMock($options), + $expression, + ); + + $this->listener->preApplyExpression($event); + + $this->assertEquals($expected, $event->getExpression()); + } + + public static function providePreApplyExpressionCases(): iterable + { + yield 'Using "trim" option' => [ + ['trim' => true], + 'TRIM(foo) = TRIM(bar)', + ]; + + yield 'Using "lower" option' => [ + ['lower' => true], + 'LOWER(foo) = LOWER(bar)', + ]; + + yield 'Using "upper" option' => [ + ['upper' => true], + 'UPPER(foo) = UPPER(bar)', + ]; + + yield 'Using "expression_transformers" option' => [ + ['expression_transformers' => [new CustomExpressionTransformer()]], + 'CUSTOM(foo) = CUSTOM(bar)', + ]; + + yield 'Using "trim", "lower" and "upper" options with "expression_transformers" option' => [ + ['trim' => true, 'lower' => true, 'upper' => true, 'expression_transformers' => [new CustomExpressionTransformer()]], + 'CUSTOM(UPPER(LOWER(TRIM(foo)))) = CUSTOM(UPPER(LOWER(TRIM(bar))))', + ]; + } + + private function createFilterMock(array $options = []): FilterInterface&MockObject + { + $options += [ + 'trim' => false, + 'lower' => false, + 'upper' => false, + 'expression_transformers' => [], + ]; + + $filterConfig = $this->createMock(FilterConfigInterface::class); + $filterConfig->method('getOption')->willReturnCallback(function (string $option) use ($options) { + return $options[$option] ?? null; + }); + + $filter = $this->createMock(FilterInterface::class); + $filter->method('getConfig')->willReturn($filterConfig); + + return $filter; + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Filter/EventListener/TransformDateRangeFilterDataTest.php b/tests/Unit/Bridge/Doctrine/Orm/Filter/EventListener/TransformDateRangeFilterDataTest.php new file mode 100644 index 00000000..e1c2bd88 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Filter/EventListener/TransformDateRangeFilterDataTest.php @@ -0,0 +1,85 @@ +listener = new TransformDateRangeFilterData(); + } + + public static function providePreHandleCases(): iterable + { + yield 'empty value' => [ + 'given' => new FilterData(), + 'expected' => new FilterData(), + ]; + + yield 'value from only' => [ + 'given' => new FilterData( + value: ['from' => new \DateTime('2022-01-01 11:22:33')], + operator: Operator::Between, + ), + 'expected' => new FilterData( + value: new \DateTime('2022-01-01 00:00:00'), + operator: Operator::GreaterThanEquals, + ), + ]; + + yield 'value to only' => [ + 'given' => new FilterData( + value: ['to' => new \DateTime('2022-01-01 11:22:33')], + operator: Operator::Between, + ), + 'expected' => new FilterData( + value: new \DateTime('2022-01-02 00:00:00'), + operator: Operator::LessThan, + ), + ]; + + yield 'value from and to' => [ + 'given' => new FilterData( + value: [ + 'from' => new \DateTime('2022-01-01 11:22:33'), + 'to' => new \DateTime('2022-01-02 11:22:33'), + ], + operator: Operator::Between, + ), + 'expected' => new FilterData( + value: [ + 'from' => new \DateTime('2022-01-01 00:00:00'), + 'to' => new \DateTime('2022-01-03 00:00:00'), + ], + operator: Operator::Between, + ), + ]; + } + + #[DataProvider('providePreHandleCases')] + public function testPreHandle(FilterData $given, FilterData $expected): void + { + $event = new PreHandleEvent( + $this->createMock(DoctrineOrmProxyQueryInterface::class), + $given, + $this->createMock(FilterInterface::class), + ); + + $this->listener->preHandle($event); + + $this->assertEquals($expected, $event->getData()); + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionFactory/ExpressionFactoryTest.php b/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionFactory/ExpressionFactoryTest.php new file mode 100644 index 00000000..a3905f22 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionFactory/ExpressionFactoryTest.php @@ -0,0 +1,202 @@ +filter = $this->createMock(FilterInterface::class); + $this->query = $this->createMock(DoctrineOrmProxyQueryInterface::class); + $this->aliasResolver = $this->createMock(AliasResolverInterface::class); + + $this->query->method('getAliasResolver')->willReturn($this->aliasResolver); + } + + #[DataProvider('expectedExpressionProvider')] + public function testItCreatesExpression(string $queryPath, FilterData $data, array $parameters, mixed $expected): void + { + $this->aliasResolver->method('resolve')->willReturn($queryPath); + + $this->assertEquals($expected, $this->createExpression($data, $parameters)); + } + + public static function expectedExpressionProvider(): iterable + { + yield 'equals' => [ + 'query_path' => 'foo', + 'data' => new FilterData(null, Operator::Equals), + 'parameters' => [ + new Parameter('bar', null), + ], + 'expected' => new Comparison('foo', '=', ':bar'), + ]; + + yield 'not equals' => [ + 'query_path' => 'foo', + 'data' => new FilterData(null, Operator::NotEquals), + 'parameters' => [ + new Parameter('bar', null), + ], + 'expected' => new Comparison('foo', '<>', ':bar'), + ]; + + yield 'contains' => [ + 'query_path' => 'foo', + 'data' => new FilterData(null, Operator::Contains), + 'parameters' => [ + new Parameter('bar', null), + ], + 'expected' => new Comparison('foo', 'LIKE', ':bar'), + ]; + + yield 'not contains' => [ + 'query_path' => 'foo', + 'data' => new FilterData(null, Operator::NotContains), + 'parameters' => [ + new Parameter('bar', null), + ], + 'expected' => new Comparison('foo', 'NOT LIKE', ':bar'), + ]; + + yield 'in' => [ + 'query_path' => 'foo', + 'data' => new FilterData(null, Operator::In), + 'parameters' => [ + new Parameter('bar', null), + ], + 'expected' => new Func('foo IN', [':bar']), + ]; + + yield 'not in' => [ + 'query_path' => 'foo', + 'data' => new FilterData(null, Operator::NotIn), + 'parameters' => [ + new Parameter('bar', null), + ], + 'expected' => new Func('foo NOT IN', [':bar']), + ]; + + yield 'greater than' => [ + 'query_path' => 'foo', + 'data' => new FilterData(null, Operator::GreaterThan), + 'parameters' => [ + new Parameter('bar', null), + ], + 'expected' => new Comparison('foo', '>', ':bar'), + ]; + + yield 'greater than or equals' => [ + 'query_path' => 'foo', + 'data' => new FilterData(null, Operator::GreaterThanEquals), + 'parameters' => [ + new Parameter('bar', null), + ], + 'expected' => new Comparison('foo', '>=', ':bar'), + ]; + + yield 'less than' => [ + 'query_path' => 'foo', + 'data' => new FilterData(null, Operator::LessThan), + 'parameters' => [ + new Parameter('bar', null), + ], + 'expected' => new Comparison('foo', '<', ':bar'), + ]; + + yield 'less than or equals' => [ + 'query_path' => 'foo', + 'data' => new FilterData(null, Operator::LessThanEquals), + 'parameters' => [ + new Parameter('bar', null), + ], + 'expected' => new Comparison('foo', '<=', ':bar'), + ]; + + yield 'starts with' => [ + 'query_path' => 'foo', + 'data' => new FilterData(null, Operator::StartsWith), + 'parameters' => [ + new Parameter('bar', null), + ], + 'expected' => new Comparison('foo', 'LIKE', ':bar'), + ]; + + yield 'ends with' => [ + 'query_path' => 'foo', + 'data' => new FilterData(null, Operator::EndsWith), + 'parameters' => [ + new Parameter('bar', null), + ], + 'expected' => new Comparison('foo', 'LIKE', ':bar'), + ]; + + yield 'between' => [ + 'query_path' => 'foo', + 'data' => new FilterData(null, Operator::Between), + 'parameters' => [ + 'from' => new Parameter('bar', null), + 'to' => new Parameter('baz', null), + ], + 'expected' => 'foo BETWEEN :bar AND :baz', + ]; + } + + public function testItRequiresParameters() + { + $this->expectExceptionObject(new InvalidArgumentException('The expression factory requires at least one parameter.')); + + $this->createExpression(); + } + + public function testBetweenOperatorRequiresFromAndToParameters() + { + $this->expectExceptionObject(new InvalidArgumentException('Operator "between" requires "from" and "to" parameters.')); + + $this->createExpression(new FilterData(operator: Operator::Between), [ + new Parameter('foo', null), + new Parameter('bar', null), + ]); + } + + public function testDataWithoutOperatorUsesFilterDefaultOperator() + { + $filterConfig = $this->createMock(FilterConfigInterface::class); + $filterConfig->method('getDefaultOperator')->willReturn(Operator::NotEquals); + + $this->filter->method('getConfig')->willReturn($filterConfig); + $this->aliasResolver->method('resolve')->willReturn('foo'); + + $expression = $this->createExpression(new FilterData(), [ + new Parameter('bar', null), + ]); + + $this->assertEquals(new Comparison('foo', '<>', ':bar'), $expression); + } + + private function createExpression(FilterData $data = new FilterData(), array $parameters = []): mixed + { + return (new ExpressionFactory())->create($this->query, $data, $this->filter, $parameters); + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/CallbackExpressionTransformerTest.php b/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/CallbackExpressionTransformerTest.php new file mode 100644 index 00000000..b02103c4 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/CallbackExpressionTransformerTest.php @@ -0,0 +1,44 @@ + "$expression bar"), 'foo', 'foo bar']; + + yield [ + self::createTransformer(static function (Expr\Comparison $comparison) use ($expr) { + return $expr->eq($expr->lower($comparison->getLeftExpr()), $expr->upper($comparison->getRightExpr())); + }), + $expr->eq('foo', 'bar'), + $expr->eq($expr->lower('foo'), $expr->upper('bar')), + ]; + } + + public function testConstructorConvertsCallableToClosure(): void + { + $transformer = self::createTransformer(fn () => null); + + $reflectionProperty = new \ReflectionProperty($transformer, 'callback'); + + $this->assertEquals('Closure', $reflectionProperty->getType()); + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/ComparisonExpressionTransformerTest.php b/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/ComparisonExpressionTransformerTest.php new file mode 100644 index 00000000..3c8306c1 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/ComparisonExpressionTransformerTest.php @@ -0,0 +1,36 @@ +transformer = new class() extends AbstractComparisonExpressionTransformer {}; + } + + public function testItThrowsExceptionWhenExpressionIsNotComparison(): void + { + $expression = 'foo = bar'; + + $this->expectExceptionObject(new UnexpectedTypeException($expression, Comparison::class)); + + $this->transformer->transform($expression); + } + + public function testItDoesNotModifyExpressionByDefault(): void + { + $comparison = new Comparison('foo', '=', 'bar'); + + $this->assertEquals($comparison, $this->transformer->transform($comparison)); + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/LowerExpressionTransformerTest.php b/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/LowerExpressionTransformerTest.php new file mode 100644 index 00000000..f41cada0 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/LowerExpressionTransformerTest.php @@ -0,0 +1,31 @@ +eq('a', 'b'), $expr->eq($expr->lower('a'), $expr->lower('b'))]; + + yield [self::createTransformer(false), $expr->eq('a', 'b'), $expr->eq('a', $expr->lower('b'))]; + + yield [self::createTransformer(true, false), $expr->eq('a', 'b'), $expr->eq($expr->lower('a'), 'b')]; + + yield [self::createTransformer(false, false), $expr->eq('a', 'b'), $expr->eq('a', 'b')]; + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/TrimExpressionTransformerTest.php b/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/TrimExpressionTransformerTest.php new file mode 100644 index 00000000..1cb56efe --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/TrimExpressionTransformerTest.php @@ -0,0 +1,31 @@ +eq('a', 'b'), $expr->eq($expr->trim('a'), $expr->trim('b'))]; + + yield [self::createTransformer(false), $expr->eq('a', 'b'), $expr->eq('a', $expr->trim('b'))]; + + yield [self::createTransformer(true, false), $expr->eq('a', 'b'), $expr->eq($expr->trim('a'), 'b')]; + + yield [self::createTransformer(false, false), $expr->eq('a', 'b'), $expr->eq('a', 'b')]; + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/UpperExpressionTransformerTest.php b/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/UpperExpressionTransformerTest.php new file mode 100644 index 00000000..eab09890 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionTransformer/UpperExpressionTransformerTest.php @@ -0,0 +1,31 @@ +eq('a', 'b'), $expr->eq($expr->upper('a'), $expr->upper('b'))]; + + yield [self::createTransformer(false), $expr->eq('a', 'b'), $expr->eq('a', $expr->upper('b'))]; + + yield [self::createTransformer(true, false), $expr->eq('a', 'b'), $expr->eq($expr->upper('a'), 'b')]; + + yield [self::createTransformer(false, false), $expr->eq('a', 'b'), $expr->eq('a', 'b')]; + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Filter/Formatter/DateActiveFilterFormatterTest.php b/tests/Unit/Bridge/Doctrine/Orm/Filter/Formatter/DateActiveFilterFormatterTest.php new file mode 100644 index 00000000..95de79c2 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Filter/Formatter/DateActiveFilterFormatterTest.php @@ -0,0 +1,50 @@ +assertEquals('123', $this->format(123)); + } + + public function testItFormatsDateTimeToDefaultFormatIfInputFormatFormOptionIsNotGiven() + { + $this->assertEquals('2023-01-01', $this->format(new \DateTime('2023-01-01'))); + } + + public function testItFormatsDateTimeUsingInputFormatFormOption() + { + $this->assertEquals('2023/01/01', $this->format(new \DateTime('2023-01-01'), [ + 'form_options' => [ + 'input_format' => 'Y/m/d', + ], + ])); + } + + private function format(mixed $value, array $filterOptions = []): string + { + $formatter = new DateActiveFilterFormatter(); + $data = new FilterData($value); + + $filter = $this->createMock(FilterInterface::class); + $filterConfig = $this->createMock(FilterConfigInterface::class); + + $filterConfig->method('getOption')->willReturnCallback(function (string $name) use ($filterOptions) { + return $filterOptions[$name] ?? null; + }); + + $filter->method('getConfig')->willReturn($filterConfig); + + return ($formatter)($data, $filter); + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Filter/Formatter/DateRangeActiveFilterFormatterTest.php b/tests/Unit/Bridge/Doctrine/Orm/Filter/Formatter/DateRangeActiveFilterFormatterTest.php new file mode 100644 index 00000000..4f925be3 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Filter/Formatter/DateRangeActiveFilterFormatterTest.php @@ -0,0 +1,65 @@ +formatter = new DateRangeActiveFilterFormatter(); + } + + public function testDateRangeWithFromDateOnly() + { + $filterData = new FilterData(['from' => $dateFrom = new \DateTime('2023-01-01'), 'to' => null]); + + $result = ($this->formatter)($filterData); + + $this->assertEquals( + new TranslatableMessage('After %date%', ['%date%' => $dateFrom->format('Y-m-d')], 'KreyuDataTable'), + $result, + ); + } + + public function testDateRangeWithToDateOnly() + { + $filterData = new FilterData(['from' => null, 'to' => $dateTo = new \DateTime('2023-01-01')]); + + $result = ($this->formatter)($filterData); + + $this->assertEquals( + new TranslatableMessage('Before %date%', ['%date%' => $dateTo->format('Y-m-d')], 'KreyuDataTable'), + $result, + ); + } + + public function testDateRangeWithEqualDates() + { + $date = new \DateTime('2023-01-20'); + $filterData = new FilterData(['from' => $date, 'to' => $date]); + + $result = ($this->formatter)($filterData); + + $this->assertEquals('2023-01-20', $result); + } + + public function testDateRangeWithBothDates() + { + $dateFrom = new \DateTime('2023-01-01'); + $dateTo = new \DateTime('2023-01-15'); + $filterData = new FilterData(['from' => $dateFrom, 'to' => $dateTo]); + + $result = ($this->formatter)($filterData); + + $this->assertEquals('2023-01-01 - 2023-01-15', $result); + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Filter/Formatter/DateTimeActiveFilterFormatterTest.php b/tests/Unit/Bridge/Doctrine/Orm/Filter/Formatter/DateTimeActiveFilterFormatterTest.php new file mode 100644 index 00000000..6244c635 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Filter/Formatter/DateTimeActiveFilterFormatterTest.php @@ -0,0 +1,60 @@ +assertEquals('123', $this->format(123)); + } + + public function testItFormatsDateTimeToDefaultFormatIfInputFormatFormOptionIsNotGiven() + { + $this->assertEquals('2023-01-01 11:22:33', $this->format(new \DateTime('2023-01-01 11:22:33'))); + } + + public function testItFormatsDateTimeUsingInputFormatFormOption() + { + $this->assertEquals('2023/01/01 11/22/33', $this->format(new \DateTime('2023-01-01 11:22:33'), [ + 'form_options' => [ + 'input_format' => 'Y/m/d H/i/s', + ], + ])); + } + + public function testItFormatsDateTimeWithMinutesAndSecondsFormOptionsSetToFalse() + { + $this->assertEquals('2023-01-01 11', $this->format(new \DateTime('2023-01-01 11:22:33'), [ + 'form_options' => [ + 'with_minutes' => false, + 'with_seconds' => false, + ], + ])); + } + + private function format(mixed $value, array $filterOptions = []): string + { + $formatter = new DateTimeActiveFilterFormatter(); + $data = new FilterData($value); + + $filter = $this->createMock(FilterInterface::class); + $filterConfig = $this->createMock(FilterConfigInterface::class); + + $filterConfig->method('getOption')->willReturnCallback(function (string $name) use ($filterOptions) { + return $filterOptions[$name] ?? null; + }); + + $filter->method('getConfig')->willReturn($filterConfig); + + return ($formatter)($data, $filter); + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Filter/Formatter/EntityActiveFilterFormatterTest.php b/tests/Unit/Bridge/Doctrine/Orm/Filter/Formatter/EntityActiveFilterFormatterTest.php new file mode 100644 index 00000000..9cbd7518 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Filter/Formatter/EntityActiveFilterFormatterTest.php @@ -0,0 +1,46 @@ +assertEquals('123', $this->format(123)); + } + + public function testItUsesChoiceLabelOptionAsPropertyPath() + { + $this->assertEquals('bar', $this->format(['foo' => 'bar'], ['choice_label' => '[foo]'])); + } + + public function testItUsesChoiceLabelOptionCallable() + { + $this->assertEquals('bar', $this->format('foo', ['choice_label' => fn () => 'bar'])); + } + + private function format(mixed $value, array $filterOptions = []): string + { + $formatter = new EntityActiveFilterFormatter(); + $data = new FilterData($value); + + $filter = $this->createMock(FilterInterface::class); + $filterConfig = $this->createMock(FilterConfigInterface::class); + + $filterConfig->method('getOption')->willReturnCallback(function (string $name) use ($filterOptions) { + return $filterOptions[$name] ?? null; + }); + + $filter->method('getConfig')->willReturn($filterConfig); + + return ($formatter)($data, $filter); + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Filter/ParameterFactory/ParameterFactoryTest.php b/tests/Unit/Bridge/Doctrine/Orm/Filter/ParameterFactory/ParameterFactoryTest.php new file mode 100644 index 00000000..6c3c7045 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Filter/ParameterFactory/ParameterFactoryTest.php @@ -0,0 +1,162 @@ +filter = $this->createMock(FilterInterface::class); + $this->query = $this->createMock(DoctrineOrmProxyQueryInterface::class); + } + + public static function expectedParametersProvider(): iterable + { + yield 'equals' => [ + 'data' => new FilterData('foo', Operator::Equals), + 'expected' => [ + new Parameter('_0', 'foo'), + ], + ]; + + yield 'not equals' => [ + 'data' => new FilterData('foo', Operator::NotEquals), + 'expected' => [ + new Parameter('_0', 'foo'), + ], + ]; + + yield 'contains' => [ + 'data' => new FilterData('foo', Operator::Contains), + 'expected' => [ + new Parameter('_0', '%foo%'), + ], + ]; + + yield 'not contains' => [ + 'data' => new FilterData('foo', Operator::NotContains), + 'expected' => [ + new Parameter('_0', '%foo%'), + ], + ]; + + yield 'in' => [ + 'data' => new FilterData('foo', Operator::In), + 'expected' => [ + new Parameter('_0', 'foo'), + ], + ]; + + yield 'not in' => [ + 'data' => new FilterData('foo', Operator::NotIn), + 'expected' => [ + new Parameter('_0', 'foo'), + ], + ]; + + yield 'greater than' => [ + 'data' => new FilterData('foo', Operator::GreaterThan), + 'expected' => [ + new Parameter('_0', 'foo'), + ], + ]; + + yield 'greater than or equals' => [ + 'data' => new FilterData('foo', Operator::GreaterThanEquals), + 'expected' => [ + new Parameter('_0', 'foo'), + ], + ]; + + yield 'less than' => [ + 'data' => new FilterData('foo', Operator::LessThan), + 'expected' => [ + new Parameter('_0', 'foo'), + ], + ]; + + yield 'less than or equals' => [ + 'data' => new FilterData('foo', Operator::LessThanEquals), + 'expected' => [ + new Parameter('_0', 'foo'), + ], + ]; + + yield 'starts with' => [ + 'data' => new FilterData('foo', Operator::StartsWith), + 'expected' => [ + new Parameter('_0', 'foo%'), + ], + ]; + + yield 'ends with' => [ + 'data' => new FilterData('foo', Operator::EndsWith), + 'expected' => [ + new Parameter('_0', '%foo'), + ], + ]; + + yield 'between with "from" and "to" in value' => [ + 'data' => new FilterData(['from' => 'foo', 'to' => 'bar'], Operator::Between), + 'expected' => [ + 'from' => new Parameter('_0_from', 'foo'), + 'to' => new Parameter('_0_to', 'bar'), + ], + ]; + + yield 'between with "from" only in value' => [ + 'data' => new FilterData(['from' => 'foo'], Operator::Between), + 'expected' => [ + 'from' => new Parameter('_0_from', 'foo'), + ], + ]; + + yield 'between with "to" only in value' => [ + 'data' => new FilterData(['to' => 'foo'], Operator::Between), + 'expected' => [ + 'to' => new Parameter('_0_to', 'foo'), + ], + ]; + + yield 'between without neither "from" or "to" in value' => [ + 'data' => new FilterData([], Operator::Between), + 'expected' => [], + ]; + } + + public function testItCreatesParametersWithFilterNameAndUniqueParameterId() + { + $this->filter->method('getName')->willReturn('foo'); + $this->query->method('getUniqueParameterId')->willReturn(10); + + $parameters = $this->createParameters(); + + $this->assertEquals('foo_10', $parameters[0]->getName()); + } + + #[DataProvider('expectedParametersProvider')] + public function testItCreatesParametersBasedOnFilterData(FilterData $data, array $expected): void + { + $this->assertEquals($expected, $this->createParameters($data)); + } + + private function createParameters(FilterData $data = new FilterData()): array + { + return (new ParameterFactory())->create($this->query, $data, $this->filter); + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Filter/Type/BooleanFilterTypeTest.php b/tests/Unit/Bridge/Doctrine/Orm/Filter/Type/BooleanFilterTypeTest.php new file mode 100644 index 00000000..22ac06f9 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Filter/Type/BooleanFilterTypeTest.php @@ -0,0 +1,78 @@ +createFilter(); + + $formatter = $filter->getConfig()->getOption('active_filter_formatter'); + + $this->assertEquals(new TranslatableMessage('Yes', domain: 'KreyuDataTable'), $formatter(new FilterData(true))); + $this->assertEquals(new TranslatableMessage('No', domain: 'KreyuDataTable'), $formatter(new FilterData(false))); + } + + public function testItShouldAddChoicesAndChoiceTranslationDomainFormOptionsWhenFormTypeIsChoiceType(): void + { + $formOptions = ['trim' => false]; + + $filter = $this->createFilter(['form_options' => $formOptions]); + + $expectedFormOptions = $formOptions + [ + 'choices' => ['Yes' => true, 'No' => false], + 'choice_translation_domain' => 'KreyuDataTable', + ]; + + $this->assertEquals($expectedFormOptions, $filter->getConfig()->getOption('form_options')); + } + + public function testItShouldNotOverwriteChoicesAndChoiceTranslationDomainFormOptionsIfGivenWhenFormTypeIsChoiceType(): void + { + $formOptions = [ + 'choices' => ['True' => true, 'False' => false], + 'choice_translation_domain' => 'App', + ]; + + $filter = $this->createFilter(['form_options' => $formOptions]); + + $this->assertEquals($formOptions, $filter->getConfig()->getOption('form_options')); + } + + public function testItShouldNotModifyFormOptionsWhenFormTypeIsNotChoiceType(): void + { + $formOptions = ['trim' => false]; + + $filter = $this->createFilter(['form_type' => TextType::class, 'form_options' => $formOptions]); + + $this->assertEquals($formOptions, $filter->getConfig()->getOption('form_options')); + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Filter/Type/DateFilterTypeTest.php b/tests/Unit/Bridge/Doctrine/Orm/Filter/Type/DateFilterTypeTest.php new file mode 100644 index 00000000..8b3bb0bd --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Filter/Type/DateFilterTypeTest.php @@ -0,0 +1,64 @@ + false]; + + $filter = $this->createFilter(['form_options' => $formOptions]); + + $expectedFormOptions = $formOptions + ['widget' => 'single_text']; + + $this->assertEquals($expectedFormOptions, $filter->getConfig()->getOption('form_options')); + } + + public function testItShouldNotOverwriteWidgetFormOptionIfGivenWhenFormTypeIsDateType(): void + { + $formOptions = ['widget' => 'choice']; + + $filter = $this->createFilter(['form_options' => $formOptions]); + + $this->assertEquals($formOptions, $filter->getConfig()->getOption('form_options')); + } + + public function testItShouldNotModifyFormOptionsWhenFormTypeIsNotDateType(): void + { + $formOptions = ['trim' => false]; + + $filter = $this->createFilter(['form_type' => TextType::class, 'form_options' => $formOptions]); + + $this->assertEquals($formOptions, $filter->getConfig()->getOption('form_options')); + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Filter/Type/DateRangeFilterTypeTest.php b/tests/Unit/Bridge/Doctrine/Orm/Filter/Type/DateRangeFilterTypeTest.php new file mode 100644 index 00000000..3dee0411 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Filter/Type/DateRangeFilterTypeTest.php @@ -0,0 +1,34 @@ + false]; + + $filter = $this->createFilter(['form_options' => $formOptions]); + + $expectedFormOptions = $formOptions + ['widget' => 'single_text']; + + $this->assertEquals($expectedFormOptions, $filter->getConfig()->getOption('form_options')); + } + + public function testItShouldNotOverwriteWidgetFormOptionIfGivenWhenFormTypeIsDateType(): void + { + $formOptions = ['widget' => 'choice']; + + $filter = $this->createFilter(['form_options' => $formOptions]); + + $this->assertEquals($formOptions, $filter->getConfig()->getOption('form_options')); + } + + public function testItShouldNotModifyFormOptionsWhenFormTypeIsNotDateType(): void + { + $formOptions = ['trim' => false]; + + $filter = $this->createFilter(['form_type' => TextType::class, 'form_options' => $formOptions]); + + $this->assertEquals($formOptions, $filter->getConfig()->getOption('form_options')); + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Filter/Type/DoctrineOrmFilterTypeTestCase.php b/tests/Unit/Bridge/Doctrine/Orm/Filter/Type/DoctrineOrmFilterTypeTestCase.php new file mode 100644 index 00000000..d954bac4 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Filter/Type/DoctrineOrmFilterTypeTestCase.php @@ -0,0 +1,34 @@ +assertEquals($this->getDefaultOperator(), $this->createFilter()->getConfig()->getDefaultOperator()); + } + + public function testItShouldSupportOperators() + { + $this->assertEquals($this->getSupportedOperators(), $this->createFilter()->getConfig()->getSupportedOperators()); + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Filter/Type/EntityFilterTypeTest.php b/tests/Unit/Bridge/Doctrine/Orm/Filter/Type/EntityFilterTypeTest.php new file mode 100644 index 00000000..0bd9f132 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Filter/Type/EntityFilterTypeTest.php @@ -0,0 +1,124 @@ +classMetadata = $this->createMock(ClassMetadata::class); + + $manager = $this->createMock(ObjectManager::class); + $manager->method('getClassMetadata')->willReturn($this->classMetadata); + + $this->managerRegistry = $this->createMock(ManagerRegistry::class); + $this->managerRegistry->method('getManagerForClass')->willReturn($manager); + + parent::setUp(); + } + + protected function getTestedType(): string + { + return EntityFilterType::class; + } + + protected function getSupportedOperators(): array + { + return [ + Operator::Equals, + Operator::NotEquals, + Operator::In, + Operator::NotIn, + ]; + } + + protected function getDefaultFormType(): string + { + return EntityType::class; + } + + protected function getExtensions(): array + { + $type = new EntityFilterType($this->managerRegistry); + + return [ + new PreloadedFilterExtension([$type], []), + ]; + } + + public function testItShouldNotModifyFormOptionsWhenFormTypeIsNotEntityType() + { + $formOptions = ['trim' => false]; + + $filter = $this->createFilter(['form_type' => TextType::class, 'form_options' => $formOptions]); + + $this->assertEquals($formOptions, $filter->getConfig()->getOption('form_options')); + } + + public function testItShouldNotModifyFormOptionsWhenClassFormOptionIsNotGiven() + { + $formOptions = ['trim' => false]; + + $filter = $this->createFilter(['form_options' => $formOptions]); + + $this->assertEquals($formOptions, $filter->getConfig()->getOption('form_options')); + } + + public function testItShouldAddChoiceValueFormOptionWhenFormTypeIsEntityTypeAndClassFormOptionIsGiven(): void + { + $this->classMetadata->method('getIdentifier')->willReturn(['id']); + + $filter = $this->createFilter([ + 'form_options' => $formOptions = [ + 'class' => 'App\\Entity\\Product', + ], + ]); + + $expectedFormOptions = $formOptions + ['choice_value' => 'id']; + + $this->assertEquals($expectedFormOptions, $filter->getConfig()->getOption('form_options')); + } + + public function testItShouldNotAddChoiceValueFormOptionWhenClassMetadataReturnsMultipleIdentifiers(): void + { + $this->classMetadata->method('getIdentifier')->willReturn(['id', 'uuid']); + + $filter = $this->createFilter([ + 'form_options' => $formOptions = [ + 'class' => 'App\\Entity\\Product', + ], + ]); + + $this->assertEquals($formOptions, $filter->getConfig()->getOption('form_options')); + } + + public function testItShouldNotOverwriteChoiceValueFormOptionIfAlreadyGiven(): void + { + $this->classMetadata->method('getIdentifier')->willReturn(['id']); + + $filter = $this->createFilter([ + 'form_options' => $formOptions = [ + 'class' => 'App\\Entity\\Product', + 'choice_value' => 'uuid', + ], + ]); + + $this->assertEquals($formOptions, $filter->getConfig()->getOption('form_options')); + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Filter/Type/NumericFilterTypeTest.php b/tests/Unit/Bridge/Doctrine/Orm/Filter/Type/NumericFilterTypeTest.php new file mode 100644 index 00000000..ce7eabe0 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Filter/Type/NumericFilterTypeTest.php @@ -0,0 +1,34 @@ +name; + } + + public function getYear(): int + { + return $this->year; + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Fixtures/Entity/Category.php b/tests/Unit/Bridge/Doctrine/Orm/Fixtures/Entity/Category.php new file mode 100644 index 00000000..f68b3ef7 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Fixtures/Entity/Category.php @@ -0,0 +1,29 @@ +id; + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Fixtures/Entity/Product.php b/tests/Unit/Bridge/Doctrine/Orm/Fixtures/Entity/Product.php new file mode 100644 index 00000000..bf9719a4 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Fixtures/Entity/Product.php @@ -0,0 +1,36 @@ +id; + } + + public function getName(): string + { + return $this->name; + } + + public function getCategory(): Category + { + return $this->category; + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Fixtures/Entity/ProductAttribute.php b/tests/Unit/Bridge/Doctrine/Orm/Fixtures/Entity/ProductAttribute.php new file mode 100644 index 00000000..d4049430 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Fixtures/Entity/ProductAttribute.php @@ -0,0 +1,29 @@ +product; + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Fixtures/Filter/ExpressionTransformer/CustomExpressionTransformer.php b/tests/Unit/Bridge/Doctrine/Orm/Fixtures/Filter/ExpressionTransformer/CustomExpressionTransformer.php new file mode 100644 index 00000000..f22e0ce9 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Fixtures/Filter/ExpressionTransformer/CustomExpressionTransformer.php @@ -0,0 +1,20 @@ + 'pdo_sqlite', + 'memory' => true, + ], $config); + + return new EntityManager( + $connection, + $config, + new EventManager(), + ); + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Paginator/PaginatorFactoryTest.php b/tests/Unit/Bridge/Doctrine/Orm/Paginator/PaginatorFactoryTest.php new file mode 100644 index 00000000..449b2739 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Paginator/PaginatorFactoryTest.php @@ -0,0 +1,154 @@ +paginatorFactory = new PaginatorFactory(); + } + + public function testCreateWithQueryBuilderWithoutRootEntities(): void + { + $queryBuilder = TestEntityManagerFactory::create()->createQueryBuilder(); + + $this->expectExceptionObject(new \RuntimeException('There are no root entities defined in the query.')); + + $this->paginatorFactory->create($queryBuilder); + } + + public function testCreateWithoutJoins(): void + { + $queryBuilder = TestEntityManagerFactory::create() + ->createQueryBuilder() + ->from(Product::class, 'product'); + + $paginator = $this->paginatorFactory->create($queryBuilder); + + $this->assertFalse($paginator->getQuery()->getHint(CountWalker::HINT_DISTINCT)); + } + + public function testCreateWithHints(): void + { + $queryBuilder = TestEntityManagerFactory::create() + ->createQueryBuilder() + ->from(Product::class, 'product'); + + $paginator = $this->paginatorFactory->create($queryBuilder, ['foo' => 'bar']); + + $this->assertEquals('bar', $paginator->getQuery()->getHint('foo')); + } + + public function testCreateWithSingleIdentifierAndJoins() + { + $queryBuilder = TestEntityManagerFactory::create() + ->createQueryBuilder() + ->from(Product::class, 'product') + ->leftJoin('product.category', 'category'); + + $paginator = $this->paginatorFactory->create($queryBuilder); + + $this->assertTrue($paginator->getFetchJoinCollection()); + $this->assertFalse($paginator->getUseOutputWalkers()); + } + + public function testCreateWithCompositeIdentifier() + { + $queryBuilder = TestEntityManagerFactory::create() + ->createQueryBuilder() + ->from(Car::class, 'car'); + + $paginator = $this->paginatorFactory->create($queryBuilder); + + $this->assertFalse($paginator->getFetchJoinCollection()); + $this->assertNull($paginator->getUseOutputWalkers()); + } + + public function testCreateWithCompositeIdentifierAndJoins() + { + $queryBuilder = TestEntityManagerFactory::create() + ->createQueryBuilder() + ->from(Car::class, 'car') + ->leftJoin(Product::class, 'product'); + + $paginator = $this->paginatorFactory->create($queryBuilder); + + $this->assertFalse($paginator->getFetchJoinCollection()); + $this->assertNull($paginator->getUseOutputWalkers()); + } + + public function testCreateWithHaving() + { + $queryBuilder = TestEntityManagerFactory::create() + ->createQueryBuilder() + ->from(Product::class, 'product') + ->having('product.name <> "test"'); + + $paginator = $this->paginatorFactory->create($queryBuilder); + + $this->assertNull($paginator->getUseOutputWalkers()); + } + + public function testCreateWithMultipleFrom() + { + $queryBuilder = TestEntityManagerFactory::create() + ->createQueryBuilder() + ->from(Product::class, 'product') + ->from(Category::class, 'category'); + + $paginator = $this->paginatorFactory->create($queryBuilder); + + $this->assertNull($paginator->getUseOutputWalkers()); + } + + public function testCreateWithForeignKeyAsIdentifier() + { + $queryBuilder = TestEntityManagerFactory::create() + ->createQueryBuilder() + ->from(ProductAttribute::class, 'productAttribute'); + + $paginator = $this->paginatorFactory->create($queryBuilder); + + $this->assertNull($paginator->getUseOutputWalkers()); + } + + public function testCreateWithJoinsAndOrderBy() + { + $queryBuilder = TestEntityManagerFactory::create() + ->createQueryBuilder() + ->from(Product::class, 'product') + ->leftJoin('product.category', 'category') + ->orderBy('product.id', 'ASC'); + + $paginator = $this->paginatorFactory->create($queryBuilder); + + $this->assertFalse($paginator->getUseOutputWalkers()); + } + + public function testCreateWithOrderByAssociation() + { + $queryBuilder = TestEntityManagerFactory::create() + ->createQueryBuilder() + ->from(Product::class, 'product') + ->leftJoin('product.category', 'category') + ->orderBy('category.id', 'ASC'); + + $paginator = $this->paginatorFactory->create($queryBuilder); + + $this->assertNull($paginator->getUseOutputWalkers()); + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Query/AliasResolverTest.php b/tests/Unit/Bridge/Doctrine/Orm/Query/AliasResolverTest.php new file mode 100644 index 00000000..2d6569d1 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Query/AliasResolverTest.php @@ -0,0 +1,68 @@ +resolver = new AliasResolver(); + } + + #[DataProvider('provideResolveCases')] + public function testResolve(QueryBuilder $queryBuilder, string $queryPath, string $resolvedQueryPath): void + { + $this->assertEquals($resolvedQueryPath, $this->resolver->resolve($queryPath, $queryBuilder)); + } + + public static function provideResolveCases(): iterable + { + yield 'Without alias in query path' => [ + TestEntityManagerFactory::create() + ->createQueryBuilder() + ->from(Product::class, 'product'), + 'name', + 'product.name', + ]; + + yield 'With alias in query path' => [ + TestEntityManagerFactory::create() + ->createQueryBuilder() + ->from(Product::class, 'product') + ->leftJoin('product.category', 'category'), + 'category.name', + 'category.name', + ]; + + yield 'With query path present in SELECT part' => [ + TestEntityManagerFactory::create() + ->createQueryBuilder() + ->addSelect('UPPER(product.name) AS product_name') + ->from(Product::class, 'product') + ->leftJoin('product.category', 'category'), + 'product_name', + 'product_name', + ]; + + yield 'With query path present in SELECT part marked as HIDDEN' => [ + TestEntityManagerFactory::create() + ->createQueryBuilder() + ->addSelect('UPPER(product.name) AS HIDDEN product_name') + ->from(Product::class, 'product') + ->leftJoin('product.category', 'category'), + 'product_name', + 'product_name', + ]; + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryFactoryTest.php b/tests/Unit/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryFactoryTest.php new file mode 100644 index 00000000..8429596a --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryFactoryTest.php @@ -0,0 +1,41 @@ +factory = new DoctrineOrmProxyQueryFactory(); + } + + public function testCreatingWithSupportedData(): void + { + $queryBuilder = $this->createStub(QueryBuilder::class); + + $data = $this->factory->create($queryBuilder); + + $this->assertInstanceOf(DoctrineOrmProxyQuery::class, $data); + $this->assertEquals($queryBuilder, $data->getQueryBuilder()); + } + + public function testCreatingWithNotSupportedData(): void + { + $data = $this->createStub(Query::class); + + $this->expectExceptionObject(new UnexpectedTypeException($data, QueryBuilder::class)); + + $this->factory->create($data); + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryTest.php b/tests/Unit/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryTest.php new file mode 100644 index 00000000..66faad25 --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Query/DoctrineOrmProxyQueryTest.php @@ -0,0 +1,286 @@ +createMock(QueryBuilder::class); + $queryBuilder->expects($this->once())->method('getDQL'); + + $proxyQuery = new DoctrineOrmProxyQuery($queryBuilder); + $proxyQuery->getDQL(); + } + + public function testCloning() + { + $queryBuilder = $this->createMock(QueryBuilder::class); + + $proxyQuery = new DoctrineOrmProxyQuery($queryBuilder); + + $this->assertNotSame($proxyQuery->getQuery(), (clone $proxyQuery)->getQueryBuilder()); + } + + public function testSort() + { + $queryBuilder = TestEntityManagerFactory::create() + ->createQueryBuilder() + ->from(Product::class, 'product') + ->leftJoin('product.category', 'category') + ->orderBy('product.id', 'ASC'); + + $proxyQuery = new DoctrineOrmProxyQuery($queryBuilder); + $proxyQuery->sort(new SortingData([ + new SortingColumnData('product.name', 'ASC'), + new SortingColumnData('category.name', 'DESC'), + ])); + + $orderBy = $queryBuilder->getDQLPart('orderBy'); + + $this->assertCount(2, $orderBy); + $this->assertEquals(['product.name ASC'], $orderBy[0]->getParts()); + $this->assertEquals(['category.name DESC'], $orderBy[1]->getParts()); + } + + public function testPaginate() + { + $queryBuilder = TestEntityManagerFactory::create() + ->createQueryBuilder() + ->from(Product::class, 'product'); + + $proxyQuery = new DoctrineOrmProxyQuery($queryBuilder); + $proxyQuery->paginate(new PaginationData(1, 25)); + + $this->assertEquals(0, $queryBuilder->getFirstResult()); + $this->assertEquals(25, $queryBuilder->getMaxResults()); + } + + public function testGetResult() + { + $queryBuilder = $this->createMock(QueryBuilder::class); + + $paginator = $this->createMock(Paginator::class); + $paginator->method('getQuery')->willReturn($this->createMock(Query::class)); + + $paginationFactory = $this->createMock(PaginatorFactoryInterface::class); + $paginationFactory->expects($this->once()) + ->method('create') + ->with($queryBuilder, ['foo' => 'bar']) + ->willReturn($paginator); + + $resultSetFactory = $this->createMock(DoctrineOrmResultSetFactoryInterface::class); + $resultSetFactory->expects($this->once()) + ->method('create') + ->with($paginator, 5000); + + $proxyQuery = new DoctrineOrmProxyQuery($queryBuilder, ['foo' => 'bar']); + $proxyQuery->setResultSetFactory($resultSetFactory); + $proxyQuery->setPaginatorFactory($paginationFactory); + $proxyQuery->getResult(); + } + + public function testGetQueryBuilder() + { + $queryBuilder = $this->createMock(QueryBuilder::class); + + $proxyQuery = new DoctrineOrmProxyQuery($queryBuilder); + + $this->assertSame($queryBuilder, $proxyQuery->getQueryBuilder()); + } + + public function testGetUniqueParameterId() + { + $queryBuilder = $this->createMock(QueryBuilder::class); + + $proxyQuery = new DoctrineOrmProxyQuery($queryBuilder); + + $this->assertEquals(0, $proxyQuery->getUniqueParameterId()); + $this->assertEquals(1, $proxyQuery->getUniqueParameterId()); + $this->assertEquals(2, $proxyQuery->getUniqueParameterId()); + } + + public function testGetHints() + { + $queryBuilder = $this->createMock(QueryBuilder::class); + + $proxyQuery = new DoctrineOrmProxyQuery($queryBuilder, ['foo' => 'bar']); + + $this->assertEquals(['foo' => 'bar'], $proxyQuery->getHints()); + } + + public function testSetHint() + { + $queryBuilder = $this->createMock(QueryBuilder::class); + + $proxyQuery = new DoctrineOrmProxyQuery($queryBuilder); + $proxyQuery->setHint('foo', 'bar'); + + $this->assertEquals(['foo' => 'bar'], $proxyQuery->getHints()); + } + + public function testGetDefaultHydrationMode() + { + $queryBuilder = $this->createMock(QueryBuilder::class); + + $query = $this->createMock(Query::class); + $query->expects($this->once())->method('setHydrationMode')->with(AbstractQuery::HYDRATE_OBJECT); + + $paginator = $this->createMock(Paginator::class); + $paginator->method('getQuery')->willReturn($query); + + $paginatorFactory = $this->createMock(PaginatorFactoryInterface::class); + $paginatorFactory->method('create')->willReturn($paginator); + + $proxyQuery = new DoctrineOrmProxyQuery($queryBuilder); + $proxyQuery->setPaginatorFactory($paginatorFactory); + + $this->assertEquals(AbstractQuery::HYDRATE_OBJECT, $proxyQuery->getHydrationMode()); + + $proxyQuery->getResult(); + } + + public function testSetHydrationMode() + { + $queryBuilder = $this->createMock(QueryBuilder::class); + + $query = $this->createMock(Query::class); + $query->expects($this->once())->method('setHydrationMode')->with(AbstractQuery::HYDRATE_ARRAY); + + $paginator = $this->createMock(Paginator::class); + $paginator->method('getQuery')->willReturn($query); + + $paginatorFactory = $this->createMock(PaginatorFactoryInterface::class); + $paginatorFactory->method('create')->willReturn($paginator); + + $proxyQuery = new DoctrineOrmProxyQuery($queryBuilder); + $proxyQuery->setPaginatorFactory($paginatorFactory); + $proxyQuery->setHydrationMode(AbstractQuery::HYDRATE_ARRAY); + + $this->assertEquals(AbstractQuery::HYDRATE_ARRAY, $proxyQuery->getHydrationMode()); + + $proxyQuery->getResult(); + } + + public function testGetDefaultBatchSize() + { + $queryBuilder = $this->createMock(QueryBuilder::class); + + $proxyQuery = new DoctrineOrmProxyQuery($queryBuilder); + + $this->assertEquals(5000, $proxyQuery->getBatchSize()); + } + + public function testSetBatchSizeZero() + { + $queryBuilder = $this->createMock(QueryBuilder::class); + + $proxyQuery = new DoctrineOrmProxyQuery($queryBuilder); + + $this->expectExceptionObject(new InvalidArgumentException('The batch size must be positive.')); + + $proxyQuery->setBatchSize(0); + } + + public function testSetBatchSizeNegative() + { + $queryBuilder = $this->createMock(QueryBuilder::class); + + $proxyQuery = new DoctrineOrmProxyQuery($queryBuilder); + + $this->expectExceptionObject(new InvalidArgumentException('The batch size must be positive.')); + + $proxyQuery->setBatchSize(-1); + } + + public function testSetBatchSize() + { + $queryBuilder = $this->createMock(QueryBuilder::class); + + $proxyQuery = new DoctrineOrmProxyQuery($queryBuilder); + $proxyQuery->setBatchSize(25); + + $this->assertEquals(25, $proxyQuery->getBatchSize()); + } + + public function testGetDefaultPaginatorFactory() + { + $queryBuilder = $this->createMock(QueryBuilder::class); + + $proxyQuery = new DoctrineOrmProxyQuery($queryBuilder); + + $this->assertInstanceOf(PaginatorFactory::class, $proxyQuery->getPaginatorFactory()); + } + + public function testSetPaginatorFactory() + { + $queryBuilder = $this->createMock(QueryBuilder::class); + $aliasResolver = $this->createMock(PaginatorFactoryInterface::class); + + $proxyQuery = new DoctrineOrmProxyQuery($queryBuilder); + $proxyQuery->setPaginatorFactory($aliasResolver); + + $this->assertEquals($aliasResolver, $proxyQuery->getPaginatorFactory()); + } + + public function testGetDefaultAliasResolver() + { + $queryBuilder = $this->createMock(QueryBuilder::class); + + $proxyQuery = new DoctrineOrmProxyQuery($queryBuilder); + + $this->assertInstanceOf(AliasResolver::class, $proxyQuery->getAliasResolver()); + } + + public function testSetAliasResolver() + { + $queryBuilder = $this->createMock(QueryBuilder::class); + $aliasResolver = $this->createMock(AliasResolverInterface::class); + + $proxyQuery = new DoctrineOrmProxyQuery($queryBuilder); + $proxyQuery->setAliasResolver($aliasResolver); + + $this->assertEquals($aliasResolver, $proxyQuery->getAliasResolver()); + } + + public function testGetDefaultResultSetFactory() + { + $queryBuilder = $this->createMock(QueryBuilder::class); + + $proxyQuery = new DoctrineOrmProxyQuery($queryBuilder); + + $this->assertInstanceOf(DoctrineOrmResultSetFactory::class, $proxyQuery->getResultSetFactory()); + } + + public function testSetResultSetFactory() + { + $queryBuilder = $this->createMock(QueryBuilder::class); + $resultSetFactory = $this->createMock(DoctrineOrmResultSetFactoryInterface::class); + + $proxyQuery = new DoctrineOrmProxyQuery($queryBuilder); + $proxyQuery->setResultSetFactory($resultSetFactory); + + $this->assertEquals($resultSetFactory, $proxyQuery->getResultSetFactory()); + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Query/DoctrineOrmResultSetFactoryTest.php b/tests/Unit/Bridge/Doctrine/Orm/Query/DoctrineOrmResultSetFactoryTest.php new file mode 100644 index 00000000..5b609c5d --- /dev/null +++ b/tests/Unit/Bridge/Doctrine/Orm/Query/DoctrineOrmResultSetFactoryTest.php @@ -0,0 +1,87 @@ +createMock(Paginator::class); + $paginator->method('count')->willReturn(25); + $paginator->method('getIterator')->willReturn(new \ArrayIterator(array_fill(0, 25, 'item'))); + + $query = $this->createMock(Query::class); + $query->method('getFirstResult')->willReturn(0); + $query->method('getMaxResults')->willReturn(null); + $query->method('getEntityManager')->willReturn($this->createMock(EntityManagerInterface::class)); + + $paginator->method('getQuery')->willReturn($query); + + $factory = new DoctrineOrmResultSetFactory(); + + $resultSet = $factory->create($paginator); + + $this->assertInstanceOf(RewindableGeneratorIterator::class, $resultSet->getIterator()); + $this->assertEquals(25, iterator_count($resultSet->getIterator())); + $this->assertEquals(25, $resultSet->getCurrentPageItemCount()); + $this->assertEquals(25, $resultSet->getTotalItemCount()); + } + + public function testCreateWithLimit() + { + $paginator = $this->createMock(Paginator::class); + $paginator->method('count')->willReturn(1000); + $paginator->method('getIterator')->willReturn(new \ArrayIterator(array_fill(0, 25, 'item'))); + + $query = $this->createMock(Query::class); + $query->method('getFirstResult')->willReturn(0); + $query->method('getMaxResults')->willReturn(25); + $query->method('getEntityManager')->willReturn($this->createMock(EntityManagerInterface::class)); + + $paginator->method('getQuery')->willReturn($query); + + $factory = new DoctrineOrmResultSetFactory(); + + $resultSet = $factory->create($paginator); + + $this->assertInstanceOf(\ArrayIterator::class, $resultSet->getIterator()); + $this->assertEquals(25, iterator_count($resultSet->getIterator())); + $this->assertEquals(25, $resultSet->getCurrentPageItemCount()); + $this->assertEquals(1000, $resultSet->getTotalItemCount()); + } + + public function testCreateWithLimitGreaterThanBatchSize() + { + $paginator = $this->createMock(Paginator::class); + $paginator->method('count')->willReturn(1000); + $paginator->method('getIterator')->willReturn(new \ArrayIterator(array_fill(0, 25, 'item'))); + + $entityManager = $this->createMock(EntityManagerInterface::class); + $entityManager->expects($this->exactly(4))->method('clear'); + + $query = $this->createMock(Query::class); + $query->method('getFirstResult')->willReturn(0); + $query->method('getMaxResults')->willReturn(100); + $query->method('getEntityManager')->willReturn($entityManager); + + $paginator->method('getQuery')->willReturn($query); + + $factory = new DoctrineOrmResultSetFactory(); + + $resultSet = $factory->create($paginator, 25); + + $this->assertInstanceOf(\ArrayIterator::class, $resultSet->getIterator()); + $this->assertEquals(100, iterator_count($resultSet->getIterator())); + $this->assertEquals(100, $resultSet->getCurrentPageItemCount()); + $this->assertEquals(1000, $resultSet->getTotalItemCount()); + } +} diff --git a/tests/Unit/Column/ColumnSortUrlGeneratorTest.php b/tests/Unit/Column/ColumnSortUrlGeneratorTest.php new file mode 100644 index 00000000..c117a991 --- /dev/null +++ b/tests/Unit/Column/ColumnSortUrlGeneratorTest.php @@ -0,0 +1,106 @@ +request = $this->createMock(Request::class); + $this->request->attributes = new ParameterBag(['_route' => self::ROUTE_NAME]); + $this->request->query = new InputBag(); + + $this->requestStack = $this->createMock(RequestStack::class); + $this->requestStack->method('getCurrentRequest')->willReturn($this->request); + + $this->urlGenerator = $this->createMock(UrlGeneratorInterface::class); + } + + public function testItGeneratesUrlWithOppositeDirection(): void + { + $this->urlGenerator->expects($this->once())->method('generate')->with(self::ROUTE_NAME, [ + self::DATA_TABLE_NAME => [ + 'firstName' => 'asc', + 'middleName' => 'desc', + 'lastName' => 'asc', + ], + ]); + + $this->generate( + $this->createColumnHeaderViewMock('firstName', null), + $this->createColumnHeaderViewMock('middleName', 'asc'), + $this->createColumnHeaderViewMock('lastName', 'desc'), + ); + } + + public function testItGeneratesWithoutColumnHeaderViews(): void + { + $this->request->attributes->set('_route_params', ['id' => 1]); + $this->request->query->set('action', 'list'); + + $this->urlGenerator->expects($this->once())->method('generate')->with(self::ROUTE_NAME, [ + 'id' => 1, + 'action' => 'list', + ]); + + $this->generate(); + } + + public function testItMergesWithRouteAndQueryParameters(): void + { + $this->request->attributes->set('_route_params', ['id' => 1]); + $this->request->query->set('action', 'list'); + + $this->urlGenerator->expects($this->once())->method('generate')->with(self::ROUTE_NAME, [ + self::DATA_TABLE_NAME => [ + 'firstName' => 'asc', + ], + 'id' => 1, + 'action' => 'list', + ]); + + $this->generate( + $this->createColumnHeaderViewMock('firstName', null), + ); + } + + private function generate(MockObject&ColumnHeaderView ...$columnHeaderViews): void + { + $columnSortUrlGenerator = new ColumnSortUrlGenerator($this->requestStack, $this->urlGenerator); + $columnSortUrlGenerator->generate(...$columnHeaderViews); + } + + private function createColumnHeaderViewMock(string $name, ?string $direction): MockObject&ColumnHeaderView + { + $columnHeaderView = $this->createMock(ColumnHeaderView::class); + $columnHeaderView->parent = $this->createMock(HeaderRowView::class); + $columnHeaderView->parent->parent = $this->createMock(DataTableView::class); + $columnHeaderView->parent->parent->vars['sort_parameter_name'] = self::DATA_TABLE_NAME; + + $columnHeaderView->vars['name'] = $name; + $columnHeaderView->vars['sort_direction'] = $direction; + + return $columnHeaderView; + } +} diff --git a/tests/Unit/Filter/FilterClearUrlGeneratorTest.php b/tests/Unit/Filter/FilterClearUrlGeneratorTest.php new file mode 100644 index 00000000..11dd124a --- /dev/null +++ b/tests/Unit/Filter/FilterClearUrlGeneratorTest.php @@ -0,0 +1,115 @@ +request = $this->createMock(Request::class); + $this->request->attributes = new ParameterBag(['_route' => self::ROUTE_NAME]); + $this->request->query = new InputBag(); + + $this->requestStack = $this->createMock(RequestStack::class); + $this->requestStack->method('getCurrentRequest')->willReturn($this->request); + + $this->urlGenerator = $this->createMock(UrlGeneratorInterface::class); + } + + public function testItGenerates(): void + { + $this->urlGenerator->expects($this->once())->method('generate')->with(self::ROUTE_NAME, [ + self::DATA_TABLE_NAME => [ + 'firstName' => [ + 'value' => '', + 'operator' => null, + ], + 'middleName' => [ + 'value' => '', + 'operator' => null, + ], + 'lastName' => [ + 'value' => '', + 'operator' => null, + ], + ], + ]); + + $this->generate( + $this->createFilterViewMock('firstName'), + $this->createFilterViewMock('middleName'), + $this->createFilterViewMock('lastName'), + ); + } + + public function testItGeneratesWithoutFilterViews(): void + { + $this->request->attributes->set('_route_params', ['id' => 1]); + $this->request->query->set('action', 'list'); + + $this->urlGenerator->expects($this->once())->method('generate')->with(self::ROUTE_NAME, [ + 'id' => 1, + 'action' => 'list', + ]); + + $this->generate(); + } + + public function testItMergesWithRouteAndQueryParameters(): void + { + $this->request->attributes->set('_route_params', ['id' => 1]); + $this->request->query->set('action', 'list'); + + $this->urlGenerator->expects($this->once())->method('generate')->with(self::ROUTE_NAME, [ + self::DATA_TABLE_NAME => [ + 'firstName' => [ + 'value' => '', + 'operator' => null, + ], + ], + 'id' => 1, + 'action' => 'list', + ]); + + $this->generate( + $this->createFilterViewMock('firstName'), + ); + } + + private function generate(MockObject&FilterView ...$filterViews): void + { + $filterClearUrlGenerator = new FilterClearUrlGenerator($this->requestStack, $this->urlGenerator); + $filterClearUrlGenerator->generate(...$filterViews); + } + + private function createFilterViewMock(string $name): MockObject&FilterView + { + $filterView = $this->createMock(FilterView::class); + $filterView->parent = $this->createMock(DataTableView::class); + $filterView->parent->vars['filtration_parameter_name'] = self::DATA_TABLE_NAME; + + $filterView->vars['name'] = $name; + + return $filterView; + } +} diff --git a/tests/Unit/Filter/FilterFactoryBuilderTest.php b/tests/Unit/Filter/FilterFactoryBuilderTest.php new file mode 100644 index 00000000..45e8736f --- /dev/null +++ b/tests/Unit/Filter/FilterFactoryBuilderTest.php @@ -0,0 +1,78 @@ +factoryBuilder = new FilterFactoryBuilder(); + } + + public function testAddExtension(): void + { + $extension = new CustomFilterExtension(); + + $this->factoryBuilder->addExtension($extension); + + $factory = $this->factoryBuilder->getFilterFactory(); + + $registry = $this->getRegistry($factory); + + $extensions = $registry->getExtensions(); + + $this->assertCount(1, $extensions); + $this->assertEquals($extension, $extensions[0]); + } + + public function testAddType(): void + { + $type = new CustomFilterType(); + + $this->factoryBuilder->addType($type); + + $factory = $this->factoryBuilder->getFilterFactory(); + + $registry = $this->getRegistry($factory); + + $extensions = $registry->getExtensions(); + + $this->assertCount(1, $extensions); + $this->assertTrue($extensions[0]->hasType($type::class)); + } + + public function testAddTypeExtension(): void + { + $typeExtension = new CustomFilterTypeExtension(); + + $this->factoryBuilder->addTypeExtension($typeExtension); + + $factory = $this->factoryBuilder->getFilterFactory(); + + $registry = $this->getRegistry($factory); + + $extensions = $registry->getExtensions(); + + $this->assertCount(1, $extensions); + $this->assertEquals([$typeExtension], $extensions[0]->getTypeExtensions(CustomFilterType::class)); + } + + private function getRegistry(FilterFactoryInterface $factory): FilterRegistryInterface + { + return (new \ReflectionProperty(FilterFactory::class, 'registry'))->getValue($factory); + } +} diff --git a/tests/Unit/Filter/FilterFactoryTest.php b/tests/Unit/Filter/FilterFactoryTest.php new file mode 100644 index 00000000..4b30772c --- /dev/null +++ b/tests/Unit/Filter/FilterFactoryTest.php @@ -0,0 +1,53 @@ +factory = new FilterFactory( + new FilterRegistry([], new ResolvedFilterTypeFactory()), + ); + } + + public function testCreatingBuilder(): void + { + $builder = $this->factory->createBuilder(); + + $this->assertEquals('filter', $builder->getName()); + } + + public function testCreatingBuilderWithType(): void + { + $builder = $this->factory->createBuilder(CustomFilterType::class); + + $this->assertEquals('custom_filter', $builder->getName()); + $this->assertEquals(CustomFilterType::class, $builder->getType()->getInnerType()::class); + } + + public function testCreatingBuilderWithOptions(): void + { + $builder = $this->factory->createBuilder(CustomFilterType::class, ['foo' => 'bar']); + + $this->assertEquals('bar', $builder->getOption('foo')); + } + + public function testCreatingNamedBuilder(): void + { + $builder = $this->factory->createNamedBuilder('foo'); + + $this->assertEquals('foo', $builder->getName()); + } +} diff --git a/tests/Unit/Filter/FilterRegistryTest.php b/tests/Unit/Filter/FilterRegistryTest.php new file mode 100644 index 00000000..c386a2d4 --- /dev/null +++ b/tests/Unit/Filter/FilterRegistryTest.php @@ -0,0 +1,116 @@ +createMock(ResolvedFilterTypeFactoryInterface::class)); + } + + public function testCallingGetTypeWithNonExistentClassThrowsException(): void + { + $this->expectExceptionObject(new InvalidArgumentException('Could not load filter type "App\\InvalidFilterType": class does not exist.')); + + // @phpstan-ignore-next-line + $this->createRegistry()->getType('App\\InvalidFilterType'); + } + + public function testCallingGetTypeWithInvalidClassThrowsException(): void + { + $this->expectExceptionObject(new InvalidArgumentException(sprintf('Could not load filter type "%s": class does not implement "%s".', FilterRegistry::class, FilterTypeInterface::class))); + + // @phpstan-ignore-next-line + $this->createRegistry()->getType(FilterRegistry::class); + } + + public function testGetTypeResolvesParentUsingExtension(): void + { + $filterType = new CustomFilterType(); + $filterTypeExtension = new CustomFilterTypeExtension(); + $parentFilterType = new FilterType(); + + $extension = $this->createMock(FilterExtensionInterface::class); + + $extension + ->expects($this->exactly(2)) + ->method('hasType') + ->willReturnCallback(function (string $name) { + return match ($name) { + CustomFilterType::class, FilterType::class => true, + default => false, + }; + }); + + $extension + ->expects($this->exactly(2)) + ->method('getType') + ->willReturnCallback(function (string $name) use ($filterType, $parentFilterType) { + // @phpstan-ignore-next-line + return match ($name) { + CustomFilterType::class => $filterType, + FilterType::class => $parentFilterType, + }; + }); + + $extension + ->expects($this->exactly(2)) + ->method('getTypeExtensions') + ->willReturnCallback(function (string $name) use ($filterTypeExtension) { + return match ($name) { + CustomFilterType::class => [$filterTypeExtension], + default => [], + }; + }); + + $resolvedFilterTypeFactory = $this->createMock(ResolvedFilterTypeFactoryInterface::class); + + $resolvedFilterTypeFactory + ->expects($matcher = $this->exactly(2)) + ->method('createResolvedType') + ->willReturnCallback(function ($type, $typeExtensions, $parent) use ($matcher, $filterTypeExtension) { + // @phpstan-ignore-next-line + match ($matcher->numberOfInvocations()) { + 1 => $this->assertInstanceOf(FilterType::class, $type), + 2 => $this->assertInstanceOf(CustomFilterType::class, $type), + }; + + // @phpstan-ignore-next-line + match ($matcher->numberOfInvocations()) { + 1 => $this->assertEmpty($typeExtensions), + 2 => $this->assertEquals([$filterTypeExtension], $typeExtensions), + }; + + // @phpstan-ignore-next-line + match ($matcher->numberOfInvocations()) { + 1 => $this->assertNull($parent), + 2 => $this->assertInstanceOf(FilterTypeInterface::class, $parent->getInnerType()), + }; + + $resolvedFilterType = $this->createMock(ResolvedFilterTypeInterface::class); + $resolvedFilterType->method('getInnerType')->willReturn($type); + $resolvedFilterType->method('getParent')->willReturn($parent); + + return $resolvedFilterType; + }); + + $registry = $this->createRegistry([$extension], $resolvedFilterTypeFactory); + + $this->assertEquals($filterType, $registry->getType(CustomFilterType::class)->getInnerType()); + $this->assertEquals($parentFilterType, $registry->getType(CustomFilterType::class)->getParent()->getInnerType()); + } +} diff --git a/tests/Unit/Filter/FilterTest.php b/tests/Unit/Filter/FilterTest.php new file mode 100644 index 00000000..447d0c58 --- /dev/null +++ b/tests/Unit/Filter/FilterTest.php @@ -0,0 +1,130 @@ +eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $this->eventDispatcher->method('hasListeners')->willReturn(true); + + $this->filterConfig = $this->createMock(FilterConfigInterface::class); + $this->filterConfig->method('getEventDispatcher')->willReturn($this->eventDispatcher); + } + + public function testFormNameStripsDotsFromFilterName(): void + { + $this->filterConfig->method('getName')->willReturn('foo.bar'); + + $this->assertEquals('foo__bar', $this->createFilter()->getFormName()); + } + + public function testQueryPathDefaultsToFilterName() + { + $this->filterConfig->method('getName')->willReturn('foo'); + $this->filterConfig->method('getOption')->willReturnCallback(function (string $name, string $default) { + $this->assertEquals('query_path', $name); + $this->assertEquals('foo', $default); + + return $default; + }); + + $this->createFilter()->getQueryPath(); + } + + public function testHandleDispatchesEvents(): void + { + $filter = $this->createFilter(); + + $query = $this->createMock(ProxyQueryInterface::class); + $data = $this->createMock(FilterData::class); + + $this->eventDispatcher + ->expects($matcher = $this->exactly(2)) + ->method('dispatch') + ->willReturnCallback(function (FilterEvent $event, string $eventName) use ($matcher, $filter, $query, $data) { + // @phpstan-ignore-next-line + $this->assertInstanceOf(match ($matcher->numberOfInvocations()) { + 1 => PreHandleEvent::class, + 2 => PostHandleEvent::class, + }, $event); + + // @phpstan-ignore-next-line + $this->assertEquals(match ($matcher->numberOfInvocations()) { + 1 => FilterEvents::PRE_HANDLE, + 2 => FilterEvents::POST_HANDLE, + }, $eventName); + + $this->assertEquals($query, $event->getQuery()); + $this->assertEquals($data, $event->getData()); + $this->assertEquals($filter, $event->getFilter()); + + return $event; + }); + + $filter->handle($query, $data); + } + + public function testCreateViewCallsResolvedFilterTypeMethods() + { + $filter = $this->createFilter(); + + $data = $this->createMock(FilterData::class); + $parent = $this->createMock(DataTableView::class); + $view = $this->createMock(FilterView::class); + $resolvedFilterType = $this->createMock(ResolvedFilterTypeInterface::class); + + $this->filterConfig->method('getType')->willReturn($resolvedFilterType); + $this->filterConfig->method('getOptions')->willReturn($options = ['foo' => 'bar']); + + $resolvedFilterType + ->expects($this->once()) + ->method('createView') + ->willReturnCallback(function ($passedFilter, $passedData, $passedParent) use ($filter, $data, $parent, $view) { + $this->assertEquals($filter, $passedFilter); + $this->assertEquals($data, $passedData); + $this->assertEquals($parent, $passedParent); + + return $view; + }); + + $resolvedFilterType + ->expects($this->once()) + ->method('buildView') + ->willReturnCallback(function ($passedView, $passedFilter, $passedData, $passedOptions) use ($view, $filter, $data, $options) { + $this->assertEquals($view, $passedView); + $this->assertEquals($filter, $passedFilter); + $this->assertEquals($data, $passedData); + $this->assertEquals($options, $passedOptions); + }); + + $this->assertEquals($view, $filter->createView($data, $parent)); + } + + private function createFilter(): FilterInterface + { + return new Filter($this->filterConfig); + } +} \ No newline at end of file diff --git a/tests/Unit/Filter/Type/CallbackFilterTypeTest.php b/tests/Unit/Filter/Type/CallbackFilterTypeTest.php new file mode 100644 index 00000000..8bd281e1 --- /dev/null +++ b/tests/Unit/Filter/Type/CallbackFilterTypeTest.php @@ -0,0 +1,37 @@ +createMock(ProxyQueryInterface::class); + $expectedData = $this->createMock(FilterData::class); + + $filter = $this->expectedFilter = $this->createFilter([ + 'callback' => function (ProxyQueryInterface $query, FilterData $data, FilterInterface $filter) use ($expectedQuery, $expectedData) { + $this->assertEquals($expectedQuery, $query); + $this->assertEquals($expectedData, $data); + $this->assertEquals($this->expectedFilter, $filter); + }, + ]); + + $filter->handle($expectedQuery, $expectedData); + } + + protected function getTestedType(): string + { + return CallbackFilterType::class; + } +} diff --git a/tests/Unit/Filter/Type/FilterTypeTest.php b/tests/Unit/Filter/Type/FilterTypeTest.php new file mode 100644 index 00000000..87a6b3ed --- /dev/null +++ b/tests/Unit/Filter/Type/FilterTypeTest.php @@ -0,0 +1,314 @@ +createNamedFilter('foo'); + $filterView = $this->createFilterView($filter); + + $this->assertEquals('foo', $filterView->vars['name']); + } + + public function testLabelDefaultValueInheritsFromSentenceCasedFilterName(): void + { + $filter = $this->createNamedFilter('fooBar'); + $filterView = $this->createFilterView($filter); + + $this->assertNull($filter->getConfig()->getOption('label')); + $this->assertEquals('Foo bar', $filterView->vars['label']); + } + + public function testPassingLabelAsOption(): void + { + $filter = $this->createFilter(['label' => 'foo']); + $filterView = $this->createFilterView($filter); + + $this->assertEquals('foo', $filter->getConfig()->getOption('label')); + $this->assertEquals('foo', $filterView->vars['label']); + } + + public function testLabelTranslationParametersDefaultValue(): void + { + $filter = $this->createFilter(); + $filterView = $this->createFilterView($filter); + + $this->assertEmpty($filter->getConfig()->getOption('label_translation_parameters')); + $this->assertEmpty($filterView->vars['label_translation_parameters']); + } + + public function testPassingLabelTranslationParametersAsOption(): void + { + $labelTranslationParameters = ['foo' => 'bar']; + + $filter = $this->createFilter(['label_translation_parameters' => $labelTranslationParameters]); + $filterView = $this->createFilterView($filter); + + $this->assertEquals($labelTranslationParameters, $filter->getConfig()->getOption('label_translation_parameters')); + $this->assertEquals($labelTranslationParameters, $filterView->vars['label_translation_parameters']); + } + + public function testTranslationDomainDefaultValueInheritsFromDataTableTranslationDomain(): void + { + $dataTableView = $this->createDataTableViewMock(); + $dataTableView->vars['translation_domain'] = 'foo'; + + $filter = $this->createFilter(); + $filterView = $this->createFilterView($filter, parent: $dataTableView); + + $this->assertNull($filter->getConfig()->getOption('translation_domain')); + $this->assertEquals('foo', $filterView->vars['translation_domain']); + } + + public function testPassingTranslationDomainAsOption(): void + { + $dataTableView = $this->createDataTableViewMock(); + $dataTableView->vars['translation_domain'] = 'bar'; + + $filter = $this->createFilter(['translation_domain' => 'foo']); + $filterView = $this->createFilterView($filter, parent: $dataTableView); + + // Additionally tests whether the given translation domain is not overwritten by the data table one + + $this->assertEquals('foo', $filter->getConfig()->getOption('translation_domain')); + $this->assertEquals('foo', $filterView->vars['translation_domain']); + } + + public function testQueryPathDefaultValue(): void + { + $filter = $this->createFilter(); + $filterView = $this->createFilterView($filter); + + $this->assertNull($filter->getConfig()->getOption('query_path')); + $this->assertEquals($filter->getQueryPath(), $filterView->vars['query_path']); + } + + public function testPassingQueryPathAsOption(): void + { + $filter = $this->createFilter(['query_path' => 'foo']); + $filterView = $this->createFilterView($filter); + + $this->assertEquals('foo', $filter->getConfig()->getOption('query_path')); + $this->assertEquals('foo', $filterView->vars['query_path']); + } + + public function testFormTypeDefaultValue(): void + { + $filter = $this->createFilter(); + $filterView = $this->createFilterView($filter); + + $this->assertEquals(TextType::class, $filter->getConfig()->getFormType()); + $this->assertEquals(TextType::class, $filterView->vars['form_type']); + } + + public function testPassingFormTypeAsOption(): void + { + $filter = $this->createFilter(['form_type' => NumberType::class]); + $filterView = $this->createFilterView($filter); + + $this->assertEquals(NumberType::class, $filter->getConfig()->getFormType()); + $this->assertEquals(NumberType::class, $filterView->vars['form_type']); + } + + public function testFormOptionsDefaultValue(): void + { + $filter = $this->createFilter(); + $filterView = $this->createFilterView($filter); + + $this->assertEmpty($filter->getConfig()->getFormOptions()); + $this->assertEmpty($filterView->vars['form_options']); + } + + public function testPassingFormOptionsAsOption(): void + { + $formOptions = ['scale' => 2]; + + $filter = $this->createFilter(['form_options' => $formOptions]); + $filterView = $this->createFilterView($filter); + + $this->assertEquals($formOptions, $filter->getConfig()->getFormOptions()); + $this->assertEquals($formOptions, $filterView->vars['form_options']); + } + + public function testOperatorFormTypeDefaultValue(): void + { + $filter = $this->createFilter(); + $filterView = $this->createFilterView($filter); + + $this->assertEquals(OperatorType::class, $this->createFilter()->getConfig()->getOperatorFormType()); + $this->assertEquals(OperatorType::class, $filterView->vars['operator_form_type']); + } + + public function testPassingOperatorFormTypeAsOption(): void + { + $filter = $this->createFilter(['operator_form_type' => NumberType::class]); + $filterView = $this->createFilterView($filter); + + $this->assertEquals(NumberType::class, $filter->getConfig()->getOperatorFormType()); + $this->assertEquals(NumberType::class, $filterView->vars['operator_form_type']); + } + + public function testOperatorFormOptionsDefaultValue(): void + { + $filter = $this->createFilter(); + $filterView = $this->createFilterView($filter); + + $this->assertEmpty($filter->getConfig()->getOperatorFormOptions()); + $this->assertEmpty($filterView->vars['form_options']); + } + + public function testPassingOperatorFormOptionsAsOption(): void + { + $operatorFormOptions = ['scale' => 2]; + + $filter = $this->createFilter(['operator_form_options' => $operatorFormOptions]); + $filterView = $this->createFilterView($filter); + + $this->assertEquals($operatorFormOptions, $filter->getConfig()->getOperatorFormOptions()); + $this->assertEquals($operatorFormOptions, $filterView->vars['operator_form_options']); + } + + public function testDefaultOperatorDefaultValue(): void + { + $filter = $this->createFilter(); + $filterView = $this->createFilterView($filter); + + $this->assertEquals(Operator::Equals, $filter->getConfig()->getDefaultOperator()); + $this->assertEquals(Operator::Equals, $filterView->vars['default_operator']); + } + + public function testPassingDefaultOperatorAsOption(): void + { + $filter = $this->createFilter(['default_operator' => Operator::NotEquals]); + $filterView = $this->createFilterView($filter); + + $this->assertEquals(Operator::NotEquals, $filter->getConfig()->getDefaultOperator()); + $this->assertEquals(Operator::NotEquals, $filterView->vars['default_operator']); + } + + public function testSupportedOperatorsDefaultValue(): void + { + $filter = $this->createFilter(); + $filterView = $this->createFilterView($filter); + + $defaultOperator = $filter->getConfig()->getDefaultOperator(); + + $this->assertEquals([$defaultOperator], $filter->getConfig()->getSupportedOperators()); + $this->assertEquals([$defaultOperator], $filterView->vars['supported_operators']); + } + + public function testPassingSupportedOperatorsAsOption(): void + { + $expectedSupportedOperators = [Operator::Equals, Operator::NotEquals]; + + $filter = $this->createFilter(['supported_operators' => $expectedSupportedOperators]); + $filterView = $this->createFilterView($filter); + + $this->assertEquals($expectedSupportedOperators, $filter->getConfig()->getSupportedOperators()); + $this->assertEquals($expectedSupportedOperators, $filterView->vars['supported_operators']); + } + + public function testPassingSupportedOperatorsAsOptionInheritsDefaultOperator(): void + { + $filter = $this->createFilter([ + 'default_operator' => Operator::Contains, + 'supported_operators' => [Operator::Equals, Operator::NotEquals], + ]); + + $filterView = $this->createFilterView($filter); + + $expectedSupportedOperators = [Operator::Equals, Operator::NotEquals, Operator::Contains]; + + $this->assertEquals($expectedSupportedOperators, $filter->getConfig()->getSupportedOperators()); + $this->assertEquals($expectedSupportedOperators, $filterView->vars['supported_operators']); + } + + public function testOperatorSelectableDefaultValue(): void + { + $filter = $this->createFilter(); + $filterView = $this->createFilterView($filter); + + $this->assertFalse($filter->getConfig()->isOperatorSelectable()); + $this->assertFalse($filterView->vars['operator_selectable']); + } + + public function testPassingOperatorSelectableAsOption(): void + { + $filter = $this->createFilter(['operator_selectable' => true]); + $filterView = $this->createFilterView($filter); + + $this->assertTrue($filter->getConfig()->isOperatorSelectable()); + $this->assertTrue($filterView->vars['operator_selectable']); + } + + public function testDataAccessibleInViewVars(): void + { + $data = new FilterData('foo'); + + $filter = $this->createFilter(); + $filterView = $this->createFilterView($filter, $data); + + $this->assertEquals($data, $filterView->vars['data']); + } + + public function testValueAccessibleInViewVarsWithoutActiveFilterFormatter(): void + { + $data = new FilterData('foo'); + + $filter = $this->createFilter(); + $filterView = $this->createFilterView($filter, $data); + + $this->assertEquals('foo', $filterView->vars['value']); + } + + public function testPassingActiveFilterFormatterAsOption(): void + { + $formatter = static function (FilterData $data, FilterInterface $filter, array $options) { + return sprintf('%s_%s_%d', $data->getValue(), $filter->getName(), count($options)); + }; + + $filter = $this->createFilter([ + 'active_filter_formatter' => $formatter, + ]); + + $data = new FilterData('foo'); + + $filterView = $this->createFilterView($filter, $data); + + $expectedValue = $formatter($data, $filter, $filter->getConfig()->getOptions()); + + $this->assertEquals($expectedValue, $filterView->vars['value']); + + // Additionally tests whether the data is unmodified when formatter is present + $this->assertEquals($data, $filterView->vars['data']); + } + + public function testDataAndValueAccessibleInViewVarsWithoutActiveFilterFormatter(): void + { + $data = new FilterData('foo'); + + $filter = $this->createFilter(); + $filterView = $this->createFilterView($filter, $data); + + $this->assertEquals($data, $filterView->vars['data']); + $this->assertEquals('foo', $filterView->vars['value']); + } + + protected function getTestedType(): string + { + return FilterType::class; + } +} diff --git a/tests/Unit/Persistence/CachePersistenceAdapterTest.php b/tests/Unit/Persistence/CachePersistenceAdapterTest.php new file mode 100644 index 00000000..732786f2 --- /dev/null +++ b/tests/Unit/Persistence/CachePersistenceAdapterTest.php @@ -0,0 +1,42 @@ +createMock(PersistenceSubjectInterface::class); + $persistenceSubject->method('getDataTablePersistenceIdentifier')->willReturn('foo'); + + $this->assertEquals('kreyu_data_table_persistence_foo', CachePersistenceAdapter::getTagName($persistenceSubject)); + } + + public function testItUrlEncodesCacheKeyToPreventReservedCharactersError() + { + // The '%' is not reserved, but it should be encoded anyway to prevent overlapping of identifiers. + // This would happen when, for example, one subject had identifier set to "@", and another had "%40" (already encoded, probably an edge case). + $reservedCharacters = '{}()/\\@%'; + + $cache = $this->createMock(CacheInterface::class); + $cache->expects($this->exactly(2))->method('get')->with('products_%7B%7D%28%29%2F%5C%40%25_foo_%7B%7D%28%29%2F%5C%40%25_id_%7B%7D%28%29%2F%5C%40%25'); + + $dataTable = $this->createMock(DataTableInterface::class); + $dataTable->method('getName')->willReturn("products_$reservedCharacters"); + + $persistenceSubject = $this->createMock(PersistenceSubjectInterface::class); + $persistenceSubject->method('getDataTablePersistenceIdentifier')->willReturn("id_$reservedCharacters"); + + $adapter = new CachePersistenceAdapter($cache, "foo_$reservedCharacters"); + $adapter->write($dataTable, $persistenceSubject, null); + $adapter->read($dataTable, $persistenceSubject); + } +}