From 95d7b4d7d1f6fde919fcaca9936754faf675fb28 Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Thu, 29 Aug 2024 15:49:20 +0100 Subject: [PATCH 01/37] fix: Configurations confined TS sub-keys --- Classes/Controller/DatatableController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index 4653c2e..f056115 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -109,7 +109,7 @@ protected function getModuleSettings(): ?array ->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT) ; - if ($settings = $setup['module.'][$this->moduleName . '.']['settings.']) { + if ($settings = $setup['module.']['tx_moduledatalisting.'][$this->moduleName . '.']['settings.']) { return $settings; } From 948fae1be28fe80e15d39acbe4b3df5ab0ba8496 Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Thu, 29 Aug 2024 15:59:57 +0100 Subject: [PATCH 02/37] build(composer): Require correct typo3 packages --- composer.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 013b0bf..2465d50 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,9 @@ "source": "https://github.com/liquidlight/module_data_listing" }, "require": { - "typo3/cms-core": "^11.5" + "typo3/cms-backend": "^11.5", + "typo3/cms-core": "^11.5", + "typo3/cms-extbase": "^11.5" }, "autoload": { "psr-4": { From 9fb36c71f0c86ae1439b0029a57cbc57481a729d Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Thu, 29 Aug 2024 16:11:36 +0100 Subject: [PATCH 03/37] build: Ignore the composer.lock file --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 73061cf..78d6bec 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ vendor/ *GENERATED* + +composer.lock From 952c1d4966273ade3a47a6f5b4f23f2fa40312b2 Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Thu, 29 Aug 2024 16:35:31 +0100 Subject: [PATCH 04/37] refactor: Improve module TS structure --- Classes/Controller/DatatableController.php | 31 +++++++++++++++------- Classes/Controller/FeUsersController.php | 2 +- ext_typoscript_setup.typoscript | 16 ++++++++--- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index f056115..6483844 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -11,21 +11,26 @@ namespace LiquidLight\ModuleDataListing\Controller; -use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; -use TYPO3\CMS\Backend\View\BackendTemplateView; -use TYPO3\CMS\Extbase\Mvc\View\ViewInterface; -use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Core\Database\Query\QueryBuilder; -use TYPO3\CMS\Core\Database\ConnectionPool; +use Exception; use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Utility\PathUtility; use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Extbase\Object\ObjectManager; -use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; +use TYPO3\CMS\Extbase\Mvc\View\ViewInterface; +use TYPO3\CMS\Backend\View\BackendTemplateView; +use TYPO3\CMS\Core\Database\Query\QueryBuilder; +use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; +use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; abstract class DatatableController extends ActionController { + protected string $table; + + protected string $configurationName; + protected $defaultViewObjectName = BackendTemplateView::class; /** @@ -77,7 +82,7 @@ protected function getHeaders(array $default): array return $headers; } - if (!$additional = $this->getModuleSettings()['additionalColumns.']) { + if (!$additional = $this->getModuleSettings()['additionalColumns.'] ?? false) { return $default; } @@ -109,8 +114,13 @@ protected function getModuleSettings(): ?array ->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT) ; - if ($settings = $setup['module.']['tx_moduledatalisting.'][$this->moduleName . '.']['settings.']) { - return $settings; + if ($moduleSettings = $setup['module.']['tx_moduledatalisting.']['configuration.'][$this->configurationName . '.'] ?? false) { + return $moduleSettings ?? []; + } else { + throw new Exception(sprintf( + 'Missing expected SetupTS definition for module.tx_moduledatalisting.configuration.%s', + $this->configurationName, + )); } return null; @@ -275,6 +285,7 @@ protected function applySearch(QueryBuilder $queryBuilder, QueryBuilder $query, protected function applyJoins(QueryBuilder $queryBuilder, QueryBuilder $query): QueryBuilder { $joins = $this->getModuleSettings()['joins.']; + if (!$joins) { return $query; } diff --git a/Classes/Controller/FeUsersController.php b/Classes/Controller/FeUsersController.php index 3a2b28d..233bd77 100644 --- a/Classes/Controller/FeUsersController.php +++ b/Classes/Controller/FeUsersController.php @@ -63,7 +63,7 @@ class FeUsersController extends DatatableController * * @var string */ - protected string $moduleName = 'tx_moduledatalisting'; + protected string $configurationName = 'tx_moduledatalisting_fe_users'; /** * Init view diff --git a/ext_typoscript_setup.typoscript b/ext_typoscript_setup.typoscript index 8467c48..d8a4515 100644 --- a/ext_typoscript_setup.typoscript +++ b/ext_typoscript_setup.typoscript @@ -1,5 +1,15 @@ -module.tx_moduledatalisting { - settings { - searchableColumns := addToList(fe_users.first_name,fe_users.last_name,fe_users.username,fe_users.email,fe_users.uid) +module.tx_moduledatalisting.configuration { + default { + searchableColumns = + additionalColumns { + # table { + # some_column = Label + # } + } + } + configuration{ + tx_moduledatalisting_fe_users { + searchableColumns := addToList(fe_users.first_name,fe_users.last_name,fe_users.username,fe_users.email,fe_users.uid) + } } } From 93af079238823f946c7ccf77057b9bfca250e3b1 Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Fri, 30 Aug 2024 10:40:20 +0100 Subject: [PATCH 05/37] fix(controller): Cache headers correctly --- Classes/Controller/DatatableController.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index 6483844..b0c73b3 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -82,12 +82,12 @@ protected function getHeaders(array $default): array return $headers; } + $headers = $default; + if (!$additional = $this->getModuleSettings()['additionalColumns.'] ?? false) { - return $default; + return $headers; } - $headers = $default; - // Apply additional headers foreach ($additional as $table => $columns) { foreach ($columns as $column => $label) { From 6235e1ed5db5845e6577af707d9aef2f231ec602 Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Fri, 30 Aug 2024 10:42:01 +0100 Subject: [PATCH 06/37] refactor(controller): Add array type to $headers --- Classes/Controller/DatatableController.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index b0c73b3..943e084 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -31,6 +31,8 @@ abstract class DatatableController extends ActionController protected string $configurationName; + protected array $headers; + protected $defaultViewObjectName = BackendTemplateView::class; /** From f99c030dc7f6be19873e3306314f8728677e1dff Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Fri, 30 Aug 2024 10:57:55 +0100 Subject: [PATCH 07/37] refactor: Remove default getHeader --- Classes/Controller/DatatableController.php | 8 ++++---- Classes/Controller/FeUsersController.php | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index 943e084..261f97b 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -76,7 +76,7 @@ protected function getConnection(string $table): Connection /** * Lookup a usergroup */ - protected function getHeaders(array $default): array + protected function getHeaders(): array { static $headers = []; @@ -84,7 +84,7 @@ protected function getHeaders(array $default): array return $headers; } - $headers = $default; + $headers = $this->headers; if (!$additional = $this->getModuleSettings()['additionalColumns.'] ?? false) { return $headers; @@ -149,7 +149,7 @@ protected function getTableData(array $params): array // Re-apply restrictions $query = $queryBuilder - ->select(...array_keys($this->getHeaders($this->headers))) + ->select(...array_keys($this->getHeaders())) ->from($this->table) ->where( $queryBuilder->expr()->eq( @@ -176,7 +176,7 @@ protected function getTableData(array $params): array $order = $params['order'][0]; if (isset($order['column']) && $order['dir']) { - $headerKeys = array_keys($this->getHeaders($this->headers)); + $headerKeys = array_keys($this->getHeaders()); // Get column to order by and use alias if present if (strpos($headerKeys[$order['column']], ' as ') !== false) { diff --git a/Classes/Controller/FeUsersController.php b/Classes/Controller/FeUsersController.php index 233bd77..4180464 100644 --- a/Classes/Controller/FeUsersController.php +++ b/Classes/Controller/FeUsersController.php @@ -155,7 +155,7 @@ function ($usergroupUid): ?string { public function indexAction(): void { $this->view->assignMultiple([ - 'headers' => array_values(parent::getHeaders($this->headers)), + 'headers' => array_values($this->getHeaders()), 'groups' => $this->getUsergroups(), ]); } From a2f7a666c2f4267715448331c315e0f79cfef84f Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Fri, 30 Aug 2024 10:58:39 +0100 Subject: [PATCH 08/37] refactor: Utilize inheritehnce with indexAction --- Classes/Controller/DatatableController.php | 10 ++++++++++ Classes/Controller/FeUsersController.php | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index 261f97b..621cba9 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -458,4 +458,14 @@ protected function applyFilters(QueryBuilder $queryBuilder, QueryBuilder $query, } return $query; } + + /** + * Default action: index + */ + public function indexAction(): void + { + $this->view->assignMultiple([ + 'headers' => array_values($this->getHeaders()), + ]); + } } diff --git a/Classes/Controller/FeUsersController.php b/Classes/Controller/FeUsersController.php index 4180464..678af1e 100644 --- a/Classes/Controller/FeUsersController.php +++ b/Classes/Controller/FeUsersController.php @@ -154,8 +154,8 @@ function ($usergroupUid): ?string { */ public function indexAction(): void { + parent::indexAction(); $this->view->assignMultiple([ - 'headers' => array_values($this->getHeaders()), 'groups' => $this->getUsergroups(), ]); } From 35e36ac0b3670f50a7165ef67e0f1a34277534ee Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Fri, 30 Aug 2024 11:41:26 +0100 Subject: [PATCH 09/37] feat: Add column SQL override to default controller --- Classes/Controller/DatatableController.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index 621cba9..61b9231 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -32,6 +32,7 @@ abstract class DatatableController extends ActionController protected string $configurationName; protected array $headers; + protected array $columnSelectOverrides = []; protected $defaultViewObjectName = BackendTemplateView::class; @@ -147,9 +148,21 @@ protected function getTableData(array $params): array ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) ; + $selectFields = array_keys($this->getHeaders()); + foreach($selectFields as $field){ + if(isset($this->columnSelectOverrides[$field])) { + $queryBuilder->addSelectLiteral(sprintf( + '%s as `%s`', + $this->columnSelectOverrides[$field], + $field, + )); + } else { + $queryBuilder->addSelect($field); + } + } + // Re-apply restrictions $query = $queryBuilder - ->select(...array_keys($this->getHeaders())) ->from($this->table) ->where( $queryBuilder->expr()->eq( From af8777a9f38a5e12fb10781401c83169a8d57bc1 Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Fri, 30 Aug 2024 13:29:49 +0100 Subject: [PATCH 10/37] refactor: Reduce query builder confusion --- Classes/Controller/DatatableController.php | 104 +++++++++++---------- 1 file changed, 55 insertions(+), 49 deletions(-) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index 61b9231..a911c5b 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -36,6 +36,13 @@ abstract class DatatableController extends ActionController protected $defaultViewObjectName = BackendTemplateView::class; + + protected ConnectionPool $connectionPool; + + public function injectConnectionPool(ConnectionPool $connectionPool) { + $this->connectionPool = $connectionPool; + } + /** * Init view and load JS */ @@ -67,10 +74,11 @@ public function initializeView(ViewInterface $view): void /** * Return query builder connection by table */ - protected function getConnection(string $table): Connection + protected function getNewQueryBuilder(): QueryBuilder { - return GeneralUtility::makeInstance(ConnectionPool::class) - ->getConnectionForTable($table) + return $this->connectionPool + ->getConnectionForTable($this->table) + ->createQueryBuilder(); ; } @@ -134,15 +142,14 @@ protected function getModuleSettings(): ?array */ protected function getTableData(array $params): array { - $connection = $this->getConnection($this->table); - $queryBuilder = $connection->createQueryBuilder(); + $query = $this->getNewQueryBuilder(); /** * Users without attached fees were not returned in the count due to null values * Removing restrictions and re-apply to fe_users only solves this * @todo TYPO3 v10+ has a cleaner way of doing this: https://docs.typo3.org/m/typo3/reference-coreapi/10.4/en-us/ApiOverview/Database/RestrictionBuilder/Index.html#limitrestrictionstotables */ - $queryBuilder + $query ->getRestrictions() ->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) @@ -151,21 +158,21 @@ protected function getTableData(array $params): array $selectFields = array_keys($this->getHeaders()); foreach($selectFields as $field){ if(isset($this->columnSelectOverrides[$field])) { - $queryBuilder->addSelectLiteral(sprintf( + $query->addSelectLiteral(sprintf( '%s as `%s`', $this->columnSelectOverrides[$field], $field, )); } else { - $queryBuilder->addSelect($field); + $query->addSelect($field); } } // Re-apply restrictions - $query = $queryBuilder + $query ->from($this->table) ->where( - $queryBuilder->expr()->eq( + $query->expr()->eq( $this->table . '.deleted', 0 ), @@ -173,11 +180,11 @@ protected function getTableData(array $params): array ; // Apply joins - $query = $this->applyJoins($queryBuilder, $query); + $query = $this->applyJoins($query, $query); // Apply filters if ($params['filters'] ?? false) { - $query = $this->applyFilters($queryBuilder, $query, $params); + $query = $this->applyFilters($query, $params); } // Page @@ -204,7 +211,7 @@ protected function getTableData(array $params): array } // Apply search - $query = $this->applySearch($queryBuilder, $query, $params); + $query = $this->applySearch($query, $params); // Page size if ($params['length'] > 0) { @@ -226,24 +233,23 @@ protected function getTableData(array $params): array */ protected function getCount(array $params): int { - $connection = $this->getConnection($this->table); - $queryBuilder = $connection->createQueryBuilder(); + $query = $this->getNewQueryBuilder(); /** * Users without attached fees were not returned in the count due to null values * Removing restrictions and re-apply to table only solves this * @todo TYPO3 v10+ has a cleaner way of doing this: https://docs.typo3.org/m/typo3/reference-coreapi/10.4/en-us/ApiOverview/Database/RestrictionBuilder/Index.html#limitrestrictionstotables */ - $queryBuilder + $query ->getRestrictions() ->removeAll() ; - $query = $queryBuilder + $query = $query ->count($this->table . '.uid') ->from($this->table) ->where( - $queryBuilder->expr()->eq( + $query->expr()->eq( $this->table . '.deleted', 0 ), @@ -251,15 +257,15 @@ protected function getCount(array $params): int ; // Apply joins - $query = $this->applyJoins($queryBuilder, $query, $this->table); + $query = $this->applyJoins($query, $this->table); // Apply filters if ($params['filters']) { - $query = $this->applyFilters($queryBuilder, $query, $params); + $query = $this->applyFilters($query, $params); } // Apply search - $query = $this->applySearch($queryBuilder, $query, $params); + $query = $this->applySearch($query, $params); $count = $query ->executeQuery() @@ -272,18 +278,18 @@ protected function getCount(array $params): int /** * Apply search to query */ - protected function applySearch(QueryBuilder $queryBuilder, QueryBuilder $query, array $params): QueryBuilder + protected function applySearch(QueryBuilder $query, array $params): QueryBuilder { if ($params['search']['value']) { $columnStr = $this->getModuleSettings()['searchableColumns']; $searchableColumns = GeneralUtility::trimExplode(',', $columnStr); - $searchQuery = $queryBuilder->expr()->orX(); + $searchQuery = $query->expr()->orX(); foreach ($searchableColumns as $field) { $searchQuery->add( - $queryBuilder->expr()->like( + $query->expr()->like( $field, - $queryBuilder->createNamedParameter('%' . $queryBuilder->escapeLikeWildcards($params['search']['value']) . '%') + $query->createNamedParameter('%' . $query->escapeLikeWildcards($params['search']['value']) . '%') ) ); } @@ -297,7 +303,7 @@ protected function applySearch(QueryBuilder $queryBuilder, QueryBuilder $query, /** * Apply joins to query */ - protected function applyJoins(QueryBuilder $queryBuilder, QueryBuilder $query): QueryBuilder + protected function applyJoins(QueryBuilder $query): QueryBuilder { $joins = $this->getModuleSettings()['joins.']; @@ -322,7 +328,7 @@ protected function applyJoins(QueryBuilder $queryBuilder, QueryBuilder $query): $this->table, $join['table'], $joinTable, - $queryBuilder->expr()->eq($joinTable . '.' . $join['localIdentifier'], $queryBuilder->quoteIdentifier($this->table . '.' . $join['foreignIdentifier'])) + $query->expr()->eq($joinTable . '.' . $join['localIdentifier'], $query->quoteIdentifier($this->table . '.' . $join['foreignIdentifier'])) ) ; break; @@ -343,15 +349,15 @@ protected function applyJoins(QueryBuilder $queryBuilder, QueryBuilder $query): $this->table, $join['table'], $joinTable, - $queryBuilder->expr()->eq($joinTable . '.' . $join['localIdentifier'], $queryBuilder->quoteIdentifier($this->table . '.uid')) + $query->expr()->eq($joinTable . '.' . $join['localIdentifier'], $query->quoteIdentifier($this->table . '.uid')) ) ->innerJoin( $joinTable, $join['secondaryTable'], $secondaryJoinTable, - $queryBuilder->expr()->andX( - $queryBuilder->expr()->eq($secondaryJoinTable . '.' . $join['secondaryLocalIdentifier'], $queryBuilder->quoteIdentifier($joinTable . '.' . $join['secondaryForeignIdentifier'])), - $queryBuilder->expr()->eq($secondaryJoinTable . '.' . $join['secondaryWhereField'], $queryBuilder->createNamedParameter($join['secondaryWhereValue'])) + $query->expr()->andX( + $query->expr()->eq($secondaryJoinTable . '.' . $join['secondaryLocalIdentifier'], $query->quoteIdentifier($joinTable . '.' . $join['secondaryForeignIdentifier'])), + $query->expr()->eq($secondaryJoinTable . '.' . $join['secondaryWhereField'], $query->createNamedParameter($join['secondaryWhereValue'])) ) ) ; @@ -364,13 +370,13 @@ protected function applyJoins(QueryBuilder $queryBuilder, QueryBuilder $query): $this->table, $join['table'], $joinTable, - $queryBuilder->expr()->eq($joinTable . '.' . $join['localIdentifier'], $queryBuilder->quoteIdentifier($this->table . '.uid')) + $query->expr()->eq($joinTable . '.' . $join['localIdentifier'], $query->quoteIdentifier($this->table . '.uid')) ) ->innerJoin( $joinTable, $join['secondaryTable'], $secondaryJoinTable, - $queryBuilder->expr()->eq($secondaryJoinTable . '.' . $join['secondaryLocalIdentifier'], $queryBuilder->quoteIdentifier($join['table'] . '.' . $join['secondaryForeignIdentifier'])) + $query->expr()->eq($secondaryJoinTable . '.' . $join['secondaryLocalIdentifier'], $query->quoteIdentifier($join['table'] . '.' . $join['secondaryForeignIdentifier'])) ) ; break; @@ -382,7 +388,7 @@ protected function applyJoins(QueryBuilder $queryBuilder, QueryBuilder $query): $this->table, $join['table'], $join['table'], - $queryBuilder->expr()->eq($join['table'] . '.' . $join['localIdentifier'], $queryBuilder->quoteIdentifier($this->table . '.' . $join['foreignIdentifier'])) + $query->expr()->eq($join['table'] . '.' . $join['localIdentifier'], $query->quoteIdentifier($this->table . '.' . $join['foreignIdentifier'])) ) ; break; @@ -397,12 +403,12 @@ protected function applyJoins(QueryBuilder $queryBuilder, QueryBuilder $query): $query = $query ->where( - $queryBuilder->expr()->orX( - $queryBuilder->expr()->eq( + $query->expr()->orX( + $query->expr()->eq( $joinTable . '.deleted', 0 ), - $queryBuilder->expr()->isNull( + $query->expr()->isNull( $joinTable . '.deleted' ), ), @@ -416,7 +422,7 @@ protected function applyJoins(QueryBuilder $queryBuilder, QueryBuilder $query): /** * Apply filters to query */ - protected function applyFilters(QueryBuilder $queryBuilder, QueryBuilder $query, array $params): QueryBuilder + protected function applyFilters(QueryBuilder $query, array $params): QueryBuilder { foreach ($params['filters'] ?? [] as $field => $filter) { // If filtering by usergroup @@ -427,18 +433,18 @@ protected function applyFilters(QueryBuilder $queryBuilder, QueryBuilder $query, if ($field === 'usergroup') { $query = $query ->andWhere( - $queryBuilder->expr()->orX( - $queryBuilder->expr()->like( + $query->expr()->orX( + $query->expr()->like( $field, - $queryBuilder->createNamedParameter($queryBuilder->escapeLikeWildcards($value) . ',%') + $query->createNamedParameter($query->escapeLikeWildcards($value) . ',%') ), - $queryBuilder->expr()->like( + $query->expr()->like( $field, - $queryBuilder->createNamedParameter('%,' . $queryBuilder->escapeLikeWildcards($value) . ',%') + $query->createNamedParameter('%,' . $query->escapeLikeWildcards($value) . ',%') ), - $queryBuilder->expr()->like( + $query->expr()->like( $field, - $queryBuilder->createNamedParameter('%,' . $queryBuilder->escapeLikeWildcards($value)) + $query->createNamedParameter('%,' . $query->escapeLikeWildcards($value)) ) ), ) @@ -446,9 +452,9 @@ protected function applyFilters(QueryBuilder $queryBuilder, QueryBuilder $query, } else { $query = $query ->andWhere( - $queryBuilder->expr()->eq( + $query->expr()->eq( $field, - $queryBuilder->createNamedParameter( + $query->createNamedParameter( $value ) ) @@ -459,9 +465,9 @@ protected function applyFilters(QueryBuilder $queryBuilder, QueryBuilder $query, } else { $query = $query ->andWhere( - $queryBuilder->expr()->eq( + $query->expr()->eq( $field, - $queryBuilder->createNamedParameter( + $query->createNamedParameter( is_array($filter) ? $filter[0] : $filter ) ) From fd84d31dbe868fb85f3657aa602da6cf0b06b4fa Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Fri, 30 Aug 2024 14:40:25 +0100 Subject: [PATCH 11/37] fix: Configure dependancy injection autowire --- Classes/Controller/FeUsersController.php | 8 ++------ Configuration/Services.yaml | 10 ++++++++++ 2 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 Configuration/Services.yaml diff --git a/Classes/Controller/FeUsersController.php b/Classes/Controller/FeUsersController.php index 678af1e..e983c13 100644 --- a/Classes/Controller/FeUsersController.php +++ b/Classes/Controller/FeUsersController.php @@ -165,10 +165,7 @@ public function indexAction(): void */ private function getUsergroups(): array { - $connection = $this->getConnection('fe_groups'); - $queryBuilder = $connection->createQueryBuilder(); - - $usergroups = $queryBuilder + $usergroups = $this->getNewQueryBuilder() ->select('title', 'uid') ->from('fe_groups') ->execute() @@ -189,8 +186,7 @@ private function getUsergroupNameByUid(int $usergroupUid): ?string return $cache[$usergroupUid]; } - $connection = $this->getConnection('fe_groups'); - $queryBuilder = $connection->createQueryBuilder(); + $queryBuilder = $this->getNewQueryBuilder(); $usergroup = $queryBuilder ->select('title') diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml new file mode 100644 index 0000000..9ba001a --- /dev/null +++ b/Configuration/Services.yaml @@ -0,0 +1,10 @@ +services: + + _defaults: + autowire: true + autoconfigure: true + public: false + + LiquidLight\ModuleDataListing\: + resource: '../Classes/*' + exclude: '../Classes/Domain/Model/*' From 0c9d97325e98d37dba49d2fcb1ff39e4a001059f Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Fri, 30 Aug 2024 14:59:07 +0100 Subject: [PATCH 12/37] feat: Add table override for getNewQueryBuilder --- Classes/Controller/DatatableController.php | 4 ++-- Classes/Controller/FeUsersController.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index a911c5b..48d1e8d 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -74,10 +74,10 @@ public function initializeView(ViewInterface $view): void /** * Return query builder connection by table */ - protected function getNewQueryBuilder(): QueryBuilder + protected function getNewQueryBuilder(?string $table = null): QueryBuilder { return $this->connectionPool - ->getConnectionForTable($this->table) + ->getConnectionForTable($table ?? $this->table) ->createQueryBuilder(); ; } diff --git a/Classes/Controller/FeUsersController.php b/Classes/Controller/FeUsersController.php index e983c13..88fd76c 100644 --- a/Classes/Controller/FeUsersController.php +++ b/Classes/Controller/FeUsersController.php @@ -165,7 +165,7 @@ public function indexAction(): void */ private function getUsergroups(): array { - $usergroups = $this->getNewQueryBuilder() + $usergroups = $this->getNewQueryBuilder('fe_groups') ->select('title', 'uid') ->from('fe_groups') ->execute() From 8acb46c0cb381baeb8fefc4beda47325bd92a472 Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Fri, 30 Aug 2024 15:00:51 +0100 Subject: [PATCH 13/37] fix: Comment @var type --- Classes/Controller/FeUsersController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Classes/Controller/FeUsersController.php b/Classes/Controller/FeUsersController.php index 88fd76c..dbe37ad 100644 --- a/Classes/Controller/FeUsersController.php +++ b/Classes/Controller/FeUsersController.php @@ -24,7 +24,6 @@ class FeUsersController extends DatatableController /** * Table * - * @var array * @access protected */ protected string $table = 'fe_users'; From 520cb80e1aedacde15774a56922509f36de29932 Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Fri, 30 Aug 2024 21:00:41 +0100 Subject: [PATCH 14/37] style: Lint PHP --- Classes/Controller/DatatableController.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index 48d1e8d..cc1103a 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -12,7 +12,6 @@ namespace LiquidLight\ModuleDataListing\Controller; use Exception; -use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Utility\PathUtility; use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -32,14 +31,15 @@ abstract class DatatableController extends ActionController protected string $configurationName; protected array $headers; + protected array $columnSelectOverrides = []; protected $defaultViewObjectName = BackendTemplateView::class; - protected ConnectionPool $connectionPool; - public function injectConnectionPool(ConnectionPool $connectionPool) { + public function injectConnectionPool(ConnectionPool $connectionPool) + { $this->connectionPool = $connectionPool; } @@ -78,7 +78,7 @@ protected function getNewQueryBuilder(?string $table = null): QueryBuilder { return $this->connectionPool ->getConnectionForTable($table ?? $this->table) - ->createQueryBuilder(); + ->createQueryBuilder() ; } @@ -156,13 +156,13 @@ protected function getTableData(array $params): array ; $selectFields = array_keys($this->getHeaders()); - foreach($selectFields as $field){ - if(isset($this->columnSelectOverrides[$field])) { + foreach ($selectFields as $field) { + if (isset($this->columnSelectOverrides[$field])) { $query->addSelectLiteral(sprintf( - '%s as `%s`', - $this->columnSelectOverrides[$field], - $field, - )); + '%s as `%s`', + $this->columnSelectOverrides[$field], + $field, + )); } else { $query->addSelect($field); } From 29fde6a55c3d97a2f0daea2be1d49a61a831b378 Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Fri, 30 Aug 2024 21:01:05 +0100 Subject: [PATCH 15/37] fix: Correct FeUsersController configuration name --- Classes/Controller/FeUsersController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Controller/FeUsersController.php b/Classes/Controller/FeUsersController.php index dbe37ad..a37827b 100644 --- a/Classes/Controller/FeUsersController.php +++ b/Classes/Controller/FeUsersController.php @@ -62,7 +62,7 @@ class FeUsersController extends DatatableController * * @var string */ - protected string $configurationName = 'tx_moduledatalisting_fe_users'; + protected string $configurationName = 'fe_users'; /** * Init view From fe913ad5507c8861a383894bf6649e496df05242 Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Fri, 30 Aug 2024 21:01:27 +0100 Subject: [PATCH 16/37] docs: Update README with v2 instructions --- README.md | 202 ++++++++++++++++++++++++++++++++++------------------ UPCOMING.md | 16 ++--- 2 files changed, 139 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 4cff4ba..a6a3d62 100644 --- a/README.md +++ b/README.md @@ -2,98 +2,76 @@ ## Overview -This package provides you with a backend datatable view of fe_users allowing you to sort, search and export your fe_users as well as filter by usergroup. +This package provides you with the tools to quickly build backend views of tables allowing you to sort, search and export your data as well as filter by related data. -By default, the following columns are shown: +Also provided is an example datatable listing for fe_users (`FeUsersController`) that can be filter by usergroup. `FeUsersController` by default will display the following columns; `ID`, `Username`, `Usergroup`, `Title`, `First name`, `Last name`, `Email`. The following columns are searchable: `ID`, `Username`, `First name`, `Last name`, `Email`. -1. ID -2. Username -3. Usergroup -4. Title -5. First name -6. Last name -7. Email +## Installation -By default, the following columns are searchable: - -1. ID -2. Username -3. First name -4. Last name -5. Email +1. `composer req liquidlight/module-data-listing` +2. Add the static include or add `@import 'EXT:module_data_listing/Configuration/TypoScript/setup'` to your TypoScript +3. Ensure the correct users have permission to view ## Settings -There are a few different settings that can be set on a site-by-site basis via typoscript. +There are a few different settings that can be set on a site-by-site basis via typoscript. These are stored in the following typoscript path `module.tx_moduledatalisting.configuration.[configuration_name]` where *`configuration_name`* is derived from a classes `$configurationName` property. -### Joins +### Headers -It is possible to bolt on data from related tables by making use of the `module.tx_moduledatalisting.settings.joins` object where: -1. `type` can be leftJoin, rightJoin or innerJoin -2. `table` is the name of the related table -3. `localIdentifier` is the unique identifier of the related table -4. `foreignIdentifier` is the matching field in the fe_users table +The `headers` key is a simple way to define what data is selected from the database and the associated column headers on the front-end. It contains key-value pairs where the key is a non-ambiguous field name and the value is the label of the column header. + +Example headers; -**Setup** ``` -module.tx_moduledatalisting { - settings { - joins { - 1 { - type = leftJoin - table = related_table - localIdentifier = uid - foreignIdentifier = related_table_uid - } - } - } +module.tx_moduledatalisting.configuration.my_configuration { + headers { + my_table\.uid => The UID of this row + my_table\.title => The title + } } ``` -### Additional columns +> [!Note] +> The dots (`.`) must be escaped as the full value is needed to perform the SQL select, i.e. `SELECT my_table.uid, my_table.title FROM ...". -It is possible to pull in additional columns from the fe_users table as well as columns from any join tables by making use of the `module.tx_moduledatalisting.settings.additionalColumns` object where: -1. `table` is the name of the table you wish to pull the additional column from (this can be fe_users or any joined tables) -2. `column` is the name of the column you wish to pull in -3. `label` is the label that is used in the datatable header +### Joins + +It is possible to add additional tables to join by making use of the `joins` object where: -**`setup`** +* The key is an alias for the table to use (this can, for the most part, be the name of the table being joined) +* Values defined as follows; + * `type` one of `join`, `leftJoin`, `rightJoin` or `innerJoin` + * `table` the table to join + * `on` the `ON` part of the join (remember to use alias names if they differ from the table's name) + +Example joins; ``` -module.tx_moduledatalisting { - settings { - additionalColumns { - table { - column = label - } - } - } +module.tx_moduledatalisting.configuration.my_configuration { + table = tx_my_table + joins { + tx_my_alias { + table = tx_my_second_table + type = leftJoin + on = tx_my_table.this_id = tx_my_alias.that_id + } + } } ``` ### Searchable columns -The default searchable columns are specified above however it is possible to add and/or remove columns from this list by making use of the `module.tx_moduledatalisting.settings.searchableColumns` object where: -1. `table` is the name of the table you wish to pull the searchable column from (this can be fe_users or any joined tables) -2. `column` is the name of the column you wish to make searchable +Datatable listings provide a search box which can be configured to search only a specific set of field. Which fields can be searched is controlled by the `searchableColumns` property, which contains a comma delimited list of fields. You can mutate this value using typoscript's [value modification functions](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/Configuration/TypoScript/Syntax/Operators/Index.html#typoscript-syntax-syntax-value-modification). -**`setup`** - -``` -module.tx_moduledatalisting { - settings { - searchableColumns := addToList(table.column1,table.column2) - searchableColumns := removeFromList(table.column3) - } -} -``` - -It is also possible to completely reset the searchable columns: +Example searchableColumns; ``` -module.tx_moduledatalisting { - settings { - searchableColumns = table.column1,table.column2 - } +module.tx_moduledatalisting.configuration.my_configuration { + # Explicitly set the columns + searchableColumns = table.column1,table.column2 + # Append additional columns + searchableColumns = addToList(table.column3,table.column4) + # Remove columns + searchableColumns = addToList(table.column2,table.column3) } ``` @@ -130,3 +108,91 @@ If you have a local JavaScript file for you custom DataTables, you can remove th + bottom2: 'buttons', + }, ``` +## Upgrading from v1 to v2 + +There number of critical differences between v1 and v2. + +### Overview of Changes + +#### Classes `LiquidLight\ModuleDataListing\Controller\DatatableController` + +* Changed property `$table` to `protected string $table` +* Changed property `$moduleName` to `protected string $configurationName` +* Changed property `$headers` to `protected array $headers` +* New property `protected array $columnSelectOverrides` maps fields to complex SQL; useful for handling computed values. +* Method `protected function getConnection(string $table): Connection` changed to `protected function getConnection(?string $table = null): Connection`. Calling without an argument uses `$this->table`. +* Method `protected function getHeaders(array $default): array` changed to `protected function getHeaders(): array`. Uses `$this->headers` internally, which was otherwise always passed-in. +* Method `indexAction(): void` implemented as per the old sub-class instructions. As a result you no longer need to define `indexAction()` to have default behaviour, you can alternatively call `parent::index()` to expand on the default behaviour. + +> [!Note] +> Previously the `$table`, `$moduleName` and `$headers` properties where not _explicitly_ defined, but where expected to be defined in sub-classes. They are now explicitly defined in this class. If you have previously extended `DatatableController` you will likely need to change your definitions to match. + +#### Class`LiquidLight\ModuleDataListing\Controller\FeUsersController` + +* Property `protected $table` changed to `protected string $table` +* Property `protected $moduleName` changed to `protected string $configurationName`. The value of this property has also been changed to "*fe_users*". + +#### Setup TS + +Previously the configuration of a datatable listing was stored in `module.[ext].settings`; essentially this would limit how many listings could be setup per-extension and stifled extensibility and clutter a key reserved for module-level settings. The values previously defined there (`joins`, `additionalColumns`, etc) have been moved to `module.tx_moduledatalisting.configuration.[configuration_name]`. This coincides with a change to `LiquidLight\ModuleDataListing\Controller\DatatableController` which has had its `$moduleName` property changes to `$configurationName`, which is used to determine which configuration to use from the ones defined in TS. + +The following is now the recommended SetupTS when defining your own listing. + +``` +module.tx_moduledatalisting { + configuration{ + [configuration_name] < .default + [configuration_name] { + ... + } + } +} + +module.[tx_myextension] { + view < module.tx_moduledatalisting.view + view { + templateRootPaths.1725047881 = EXT:[my_extension]/Resources/Private/Backend/Templates/ + layoutRootPaths.1725047881 = EXT:[my_extension]/Resources/Private/Backend/Layouts/ + partialRootPaths.1725047881 = EXT:[my_extension]/Resources/Private/Backend/Partials/ + } +} +``` + +> [!Note] +> The `FeUsersController` class is no longer tied to the "default" configuration, but rather its own. located in `module.tx_moduledatalisting.configuration.fe_users` it still inherits from the `module.tx_moduledatalisting.configuration.default`, as is recommended in the block above. + +Joins are defined and processed differently in version 2. The previous numerical index for joins has been replaced with a string key, that represents the alias of the joined table. The previous `localIdentifier` and `foreignIdentifier` keys have been simplified into a single `on` key, which defined the entire on statement. + +``` +module.tx_moduledatalisting { + configuration { + fe_user_groups < .fe_users + fe_user_groups { + joins { + fe_groups { + type = join + table = fe_groups + on = FIND_IN_SET(fe_groups.uid, fe_users.usergroup) + } + } + } + } +} +``` + +#### Extensibility changes + +In version 1 there was a number of esoteric configurations when extending `DatatableController`: the `$table` class property set the table to use, while joins where defined in typoscript; SQL select and HTML column headers were computed from `$headers` and typoscript; Fields to search where entirely handled in TS. To make these options more consistent they can now all be set in _either_ typoscript or on an extending class, and have priority respectively. + +The following in a breakdown of the class properties and their respective typoscript. Note that class properties are all defined in `DatatableListing` while the typoscript keys are relative to `module.tx_moduledatalisting.configuration.[configuration_name]`, where *`configuration_name`* is derived from a classes `$configurationName` property. This is the *only* place you can set the *`configuration_name`* for a class. + +| Class Property | Typoscript Key | Description | +|--------------------------------|-------------------------------------------------|-------------| +| `string $table` | `table = ` | The SQL table name. Should be present in the TCA either as a table or as part of a column's relationship. | +| `string $searchableColumns` | `searchableColumns = [List of searchable columns]`| Comma delimited list of table columns to perform searches using | +| `array $headers` | `headers.[table\.column] = [Column Header]` | Key-value pair mapping a table's column to it's display header; the same as where the key is a non-ambiguious `$header` property used to require. You will need to escape dots (`.`), i.e. `fe_users\.username = Username`. | +| `array $columnSelectOverrides` | `columnSelectOverrides.[table\.column] = [SQL]` | Key-value pair mapping a table's column to a complex SQL expression, i.e. where the key is a non-ambiguious `fe_users\.last_name = CONCAT(fe_users.last_name, ', ', fe_users.firstname)`. When set the key `headers` and `columnSelectOverrides` values become sorting hints, in the previous example sorting would be by `last_name` | +| `array $joins` | `joins.[alias] { ... }` | As explained above in the "Setup TS" section | + +> [!Note] +> When joining tables you should use the alias in place of the table name for the purposes of `searchableColumns`, `headers`, and `columnSelectOverrides`. diff --git a/UPCOMING.md b/UPCOMING.md index ed5496b..9f64382 100644 --- a/UPCOMING.md +++ b/UPCOMING.md @@ -1,14 +1,8 @@ -# Minor +# Major #### Feature -- Upgrade DataTables to 2.x - -#### Fix - -- Load the ext path correctly -- Check if user ID is in the group before processing - -#### Refactor - -- Lots of code tidy-up, refactoring and indenting correctly +- BREAKING CHANGE: Restructure SetupTS related to `module.tx_moduledatalisting` (see README.md) +- BREAKING CHANGE: `DatatableController` has various changes to properties and methods (see README.md) +- BREAKING CHANGE: Move Typoscript to Configuration to avoid auto loading (see README.md) +- Enabled dependency injection auto-wiring in Services.yaml From b2dbb6a7dd1ad6136830d70d4358ad463a99cc50 Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Sat, 31 Aug 2024 17:29:01 +0100 Subject: [PATCH 17/37] fix: Perform OrderBy using column numbers --- Classes/Controller/DatatableController.php | 55 +++++++++++++++------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index cc1103a..2c03bc1 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -193,22 +193,7 @@ protected function getTableData(array $params): array } // Order - $order = $params['order'][0]; - - if (isset($order['column']) && $order['dir']) { - $headerKeys = array_keys($this->getHeaders()); - - // Get column to order by and use alias if present - if (strpos($headerKeys[$order['column']], ' as ') !== false) { - $column = explode(' as ', $headerKeys[$order['column']])[1]; - } else { - $column = $headerKeys[$order['column']]; - } - - $query = $query->orderBy($column, $order['dir']); - } else { - $query = $query->orderBy($this->table . '.uid', 'DESC'); - } + $this->applyOrder($query, $params); // Apply search $query = $this->applySearch($query, $params); @@ -478,6 +463,44 @@ protected function applyFilters(QueryBuilder $query, array $params): QueryBuilde return $query; } + /** + * + */ + public function applyOrder(QueryBuilder $query, array $params) + { + + $orders = $params['order'] ?? []; + $columnCount = count($this->getHeaders()); + + foreach ($orders as $order) { + $column = $order['column'] ?? false; + $dir = $order['dir'] ?? 'ASC'; + + if (!is_numeric($column)) { + continue; + } + + $column = (int)$column; + + if (!is_int($column)) { + continue; + } + + if (0 > $column || $column >= $columnCount) { + continue; + } + + if (!in_array(strtoupper($dir), ['ASC', 'DESC'], true)) { + continue; + } + + // SQL order by columns are 1-base + $query->getConcreteQueryBuilder()->addOrderBy($column + 1, $dir); + } + + return $this; + } + /** * Default action: index */ From b7d1924151f02121e0bc3642cdecfe57740dc1a4 Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Sat, 31 Aug 2024 22:11:59 +0100 Subject: [PATCH 18/37] refactor: Simplify table joins --- Classes/Controller/DatatableController.php | 135 ++++++--------------- 1 file changed, 35 insertions(+), 100 deletions(-) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index 2c03bc1..9d180fe 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -12,6 +12,7 @@ namespace LiquidLight\ModuleDataListing\Controller; use Exception; +use RuntimeException; use TYPO3\CMS\Core\Utility\PathUtility; use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -148,7 +149,7 @@ protected function getTableData(array $params): array * Users without attached fees were not returned in the count due to null values * Removing restrictions and re-apply to fe_users only solves this * @todo TYPO3 v10+ has a cleaner way of doing this: https://docs.typo3.org/m/typo3/reference-coreapi/10.4/en-us/ApiOverview/Database/RestrictionBuilder/Index.html#limitrestrictionstotables - */ + */ $query ->getRestrictions() ->removeAll() @@ -224,7 +225,7 @@ protected function getCount(array $params): int * Users without attached fees were not returned in the count due to null values * Removing restrictions and re-apply to table only solves this * @todo TYPO3 v10+ has a cleaner way of doing this: https://docs.typo3.org/m/typo3/reference-coreapi/10.4/en-us/ApiOverview/Database/RestrictionBuilder/Index.html#limitrestrictionstotables - */ + */ $query ->getRestrictions() ->removeAll() @@ -290,115 +291,49 @@ protected function applySearch(QueryBuilder $query, array $params): QueryBuilder */ protected function applyJoins(QueryBuilder $query): QueryBuilder { - $joins = $this->getModuleSettings()['joins.']; + $joins = $this->getModuleSettings()['joins.'] ?? []; - if (!$joins) { - return $query; - } + $typesAllowed = ['join', 'leftJoin', 'rightJoin', 'innerJoin']; // Apply joins from settings - foreach ($joins as $join) { - // Should we be using an alias for join? - if (array_key_exists('as', $join)) { - $joinTable = $join['as']; - } else { - $joinTable = $join['table']; + foreach ($joins as $alias => $join) { + + foreach (['table', 'type', 'on'] as $property) { + if (!isset($join[$property])) { + throw new RuntimeException(sprintf( + 'Expected join definition %s to contain %s', + $alias, + $property + )); + } } - switch ($type = $join['type']) { - case 'leftJoin': - case 'rightJoin': - $query = $query - ->$type( - $this->table, - $join['table'], - $joinTable, - $query->expr()->eq($joinTable . '.' . $join['localIdentifier'], $query->quoteIdentifier($this->table . '.' . $join['foreignIdentifier'])) - ) - ; - break; - case 'innerJoin': - // Apply many-to-many joins - if (substr($join['table'], -3) === '_mm') { - // Should we be using an alias for secondary? - if (array_key_exists('secondaryTableAs', $join)) { - $secondaryJoinTable = $join['secondaryTableAs']; - } else { - $secondaryJoinTable = $join['secondaryTable']; - } - - // Do we need to apply an additional where clause to join table? - if (array_key_exists('secondaryWhereField', $join) && array_key_exists('secondaryWhereValue', $join)) { - $query = $query - ->innerJoin( - $this->table, - $join['table'], - $joinTable, - $query->expr()->eq($joinTable . '.' . $join['localIdentifier'], $query->quoteIdentifier($this->table . '.uid')) - ) - ->innerJoin( - $joinTable, - $join['secondaryTable'], - $secondaryJoinTable, - $query->expr()->andX( - $query->expr()->eq($secondaryJoinTable . '.' . $join['secondaryLocalIdentifier'], $query->quoteIdentifier($joinTable . '.' . $join['secondaryForeignIdentifier'])), - $query->expr()->eq($secondaryJoinTable . '.' . $join['secondaryWhereField'], $query->createNamedParameter($join['secondaryWhereValue'])) - ) - ) - ; - break; - } + $alias = substr($alias, 0, -1); // Remove the trailing . from TS + $table = $join['table']; + $type = $join['type']; + $on = $join['on']; - // Apply mm join without additional where clause - $query = $query - ->innerJoin( - $this->table, - $join['table'], - $joinTable, - $query->expr()->eq($joinTable . '.' . $join['localIdentifier'], $query->quoteIdentifier($this->table . '.uid')) - ) - ->innerJoin( - $joinTable, - $join['secondaryTable'], - $secondaryJoinTable, - $query->expr()->eq($secondaryJoinTable . '.' . $join['secondaryLocalIdentifier'], $query->quoteIdentifier($join['table'] . '.' . $join['secondaryForeignIdentifier'])) - ) - ; - break; - } - - // Apply standard join - $query = $query - ->innerJoin( - $this->table, - $join['table'], - $join['table'], - $query->expr()->eq($join['table'] . '.' . $join['localIdentifier'], $query->quoteIdentifier($this->table . '.' . $join['foreignIdentifier'])) - ) - ; - break; + if (!in_array($type, $typesAllowed, true)) { + throw new RuntimeException(sprintf( + 'Unexpected join definition %s has type of %s', + $alias, + $type, + )); } - } - foreach ($joins as $join) { - // Don't check the mm tables - if (substr($join['table'], -3) === '_mm') { - continue; - } + // Perform the join + $query->$type($this->table, $table, $alias, $on); - $query = $query - ->where( + // Exclude anything that is deleted + if ($deleteFiled = $GLOBALS['TCA'][$table]['ctrl']['delete'] ?? false) { + $deleteFiled = $alias . '.' . $deleteFiled; + $query->where( $query->expr()->orX( - $query->expr()->eq( - $joinTable . '.deleted', - 0 - ), - $query->expr()->isNull( - $joinTable . '.deleted' - ), + $query->expr()->eq($deleteFiled, 0), + $query->expr()->isNull($deleteFiled), ), - ) - ; + ); + } } return $query; From 915b89a9bbcf612330a1f2959378b68aa79989ab Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Sun, 1 Sep 2024 00:11:39 +0100 Subject: [PATCH 19/37] fix: Correct user datatable TS --- ext_typoscript_setup.typoscript | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ext_typoscript_setup.typoscript b/ext_typoscript_setup.typoscript index d8a4515..e07257a 100644 --- a/ext_typoscript_setup.typoscript +++ b/ext_typoscript_setup.typoscript @@ -7,9 +7,7 @@ module.tx_moduledatalisting.configuration { # } } } - configuration{ - tx_moduledatalisting_fe_users { - searchableColumns := addToList(fe_users.first_name,fe_users.last_name,fe_users.username,fe_users.email,fe_users.uid) - } + fe_users { + searchableColumns := addToList(fe_users.first_name,fe_users.last_name,fe_users.username,fe_users.email,fe_users.uid) } } From 556c0e3b4cc1a838b46fe913b62326f3718c010d Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Sun, 1 Sep 2024 01:19:10 +0100 Subject: [PATCH 20/37] refactor: TS settings included at construction --- Classes/Controller/DatatableController.php | 108 ++++++++------------- 1 file changed, 42 insertions(+), 66 deletions(-) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index 9d180fe..337f648 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -27,21 +27,53 @@ abstract class DatatableController extends ActionController { - protected string $table; - protected string $configurationName; + protected string $table; + protected array $headers; - protected array $columnSelectOverrides = []; + protected array $columnSelectOverrides; + + protected string $searchableColumns; + + protected array $joins; protected $defaultViewObjectName = BackendTemplateView::class; protected ConnectionPool $connectionPool; - public function injectConnectionPool(ConnectionPool $connectionPool) - { + public function __construct(ConfigurationManagerInterface $configurationManagerInterface, ConnectionPool $connectionPool) { $this->connectionPool = $connectionPool; + + $setup = $configurationManagerInterface->getConfiguration( + ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT + ); + + if (!$configuration = $setup['module.']['tx_moduledatalisting.']['configuration.'][$this->configurationName . '.'] ?? false) { + throw new Exception(sprintf( + 'Missing expected SetupTS definition for module.tx_moduledatalisting.configuration.%s', + $this->configurationName, + )); + } + + $this->table = $this->table ?? $configuration['table']; + $this->headers = $this->headers ?? $configuration['headers.']; + $this->columnSelectOverrides = $this->columnSelectOverrides ?? $configuration['columnSelectOverrides.'] ?? []; + $this->joins = $this->joins ?? $configuration['joins.'] ?? []; + $this->searchableColumns = $this->searchableColumns ?? $configuration['searchableColumns'] ?? []; + + if($additionalColumns = $configuration['additionalColumns.'] ?? false) { + foreach ($additionalColumns as $table => $columns) { + foreach ($columns as $column => $label) { + if (array_key_exists($table . $column, $this->headers)) { + continue; + } + $this->headers[$table . $column] = $label; + } + } + } + } /** @@ -83,61 +115,6 @@ protected function getNewQueryBuilder(?string $table = null): QueryBuilder ; } - /** - * Lookup a usergroup - */ - protected function getHeaders(): array - { - static $headers = []; - - if (count($headers)) { - return $headers; - } - - $headers = $this->headers; - - if (!$additional = $this->getModuleSettings()['additionalColumns.'] ?? false) { - return $headers; - } - - // Apply additional headers - foreach ($additional as $table => $columns) { - foreach ($columns as $column => $label) { - if (!array_key_exists($table . $column, $headers)) { - $headers[$table . $column] = $label; - } - } - } - - return $headers; - } - - protected function getModuleSettings(): ?array - { - static $settings = []; - - if (count($settings)) { - return $settings; - } - - // Load module settings - $setup = GeneralUtility::makeInstance(ObjectManager::class) - ->get(ConfigurationManagerInterface::class) - ->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT) - ; - - if ($moduleSettings = $setup['module.']['tx_moduledatalisting.']['configuration.'][$this->configurationName . '.'] ?? false) { - return $moduleSettings ?? []; - } else { - throw new Exception(sprintf( - 'Missing expected SetupTS definition for module.tx_moduledatalisting.configuration.%s', - $this->configurationName, - )); - } - - return null; - } - /** * Get the table data */ @@ -156,7 +133,7 @@ protected function getTableData(array $params): array ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) ; - $selectFields = array_keys($this->getHeaders()); + $selectFields = array_keys($this->headers); foreach ($selectFields as $field) { if (isset($this->columnSelectOverrides[$field])) { $query->addSelectLiteral(sprintf( @@ -267,8 +244,7 @@ protected function getCount(array $params): int protected function applySearch(QueryBuilder $query, array $params): QueryBuilder { if ($params['search']['value']) { - $columnStr = $this->getModuleSettings()['searchableColumns']; - $searchableColumns = GeneralUtility::trimExplode(',', $columnStr); + $searchableColumns = GeneralUtility::trimExplode(',', $this->searchableColumns); $searchQuery = $query->expr()->orX(); foreach ($searchableColumns as $field) { @@ -291,7 +267,7 @@ protected function applySearch(QueryBuilder $query, array $params): QueryBuilder */ protected function applyJoins(QueryBuilder $query): QueryBuilder { - $joins = $this->getModuleSettings()['joins.'] ?? []; + $joins = $this->joins ?? []; $typesAllowed = ['join', 'leftJoin', 'rightJoin', 'innerJoin']; @@ -405,7 +381,7 @@ public function applyOrder(QueryBuilder $query, array $params) { $orders = $params['order'] ?? []; - $columnCount = count($this->getHeaders()); + $columnCount = count($this->headers); foreach ($orders as $order) { $column = $order['column'] ?? false; @@ -442,7 +418,7 @@ public function applyOrder(QueryBuilder $query, array $params) public function indexAction(): void { $this->view->assignMultiple([ - 'headers' => array_values($this->getHeaders()), + 'headers' => array_values($this->headers), ]); } } From 03ccc4992d32625c4263d900b2e7e7bf51d7688b Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Sun, 1 Sep 2024 01:51:07 +0100 Subject: [PATCH 21/37] refactor: Simplify logic --- Classes/Controller/DatatableController.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index 337f648..9a03d6a 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -63,14 +63,12 @@ public function __construct(ConfigurationManagerInterface $configurationManagerI $this->joins = $this->joins ?? $configuration['joins.'] ?? []; $this->searchableColumns = $this->searchableColumns ?? $configuration['searchableColumns'] ?? []; - if($additionalColumns = $configuration['additionalColumns.'] ?? false) { - foreach ($additionalColumns as $table => $columns) { - foreach ($columns as $column => $label) { - if (array_key_exists($table . $column, $this->headers)) { - continue; - } - $this->headers[$table . $column] = $label; + foreach ($configuration['additionalColumns.'] ?? [] as $table => $columns) { + foreach ($columns as $column => $label) { + if (array_key_exists($table . $column, $this->headers)) { + continue; } + $this->headers[$table . $column] = $label; } } From f153c1fd63b5f46c86f4df894a1087e1b7ceaefb Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Sun, 1 Sep 2024 11:41:38 +0100 Subject: [PATCH 22/37] refactor: Make TS configs prefered to class properties --- Classes/Controller/DatatableController.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index 9a03d6a..9533161 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -57,11 +57,11 @@ public function __construct(ConfigurationManagerInterface $configurationManagerI )); } - $this->table = $this->table ?? $configuration['table']; - $this->headers = $this->headers ?? $configuration['headers.']; - $this->columnSelectOverrides = $this->columnSelectOverrides ?? $configuration['columnSelectOverrides.'] ?? []; - $this->joins = $this->joins ?? $configuration['joins.'] ?? []; - $this->searchableColumns = $this->searchableColumns ?? $configuration['searchableColumns'] ?? []; + $this->table = $configuration['table'] ?? $this->table; + $this->headers = $configuration['headers.'] ?? $this->headers ?? []; + $this->columnSelectOverrides = $configuration['columnSelectOverrides.'] ?? $this->columnSelectOverrides ?? []; + $this->joins = $configuration['joins.'] ?? $this->joins ??[]; + $this->searchableColumns = $configuration['searchableColumns'] ?? $this->searchableColumns ?? []; foreach ($configuration['additionalColumns.'] ?? [] as $table => $columns) { foreach ($columns as $column => $label) { From 5f202968fe1afa8e91829d05f36f9e2d6c351db0 Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Sun, 1 Sep 2024 18:30:04 +0100 Subject: [PATCH 23/37] refactor: Centralize shared query logic --- Classes/Controller/DatatableController.php | 96 ++++++++-------------- 1 file changed, 36 insertions(+), 60 deletions(-) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index 9533161..a3a63c6 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -43,7 +43,8 @@ abstract class DatatableController extends ActionController protected ConnectionPool $connectionPool; - public function __construct(ConfigurationManagerInterface $configurationManagerInterface, ConnectionPool $connectionPool) { + public function __construct(ConfigurationManagerInterface $configurationManagerInterface, ConnectionPool $connectionPool) + { $this->connectionPool = $connectionPool; $setup = $configurationManagerInterface->getConfiguration( @@ -60,7 +61,7 @@ public function __construct(ConfigurationManagerInterface $configurationManagerI $this->table = $configuration['table'] ?? $this->table; $this->headers = $configuration['headers.'] ?? $this->headers ?? []; $this->columnSelectOverrides = $configuration['columnSelectOverrides.'] ?? $this->columnSelectOverrides ?? []; - $this->joins = $configuration['joins.'] ?? $this->joins ??[]; + $this->joins = $configuration['joins.'] ?? $this->joins ?? []; $this->searchableColumns = $configuration['searchableColumns'] ?? $this->searchableColumns ?? []; foreach ($configuration['additionalColumns.'] ?? [] as $table => $columns) { @@ -113,37 +114,16 @@ protected function getNewQueryBuilder(?string $table = null): QueryBuilder ; } - /** - * Get the table data - */ - protected function getTableData(array $params): array + protected function prepareQuery(array $params): QueryBuilder { $query = $this->getNewQueryBuilder(); - /** - * Users without attached fees were not returned in the count due to null values - * Removing restrictions and re-apply to fe_users only solves this - * @todo TYPO3 v10+ has a cleaner way of doing this: https://docs.typo3.org/m/typo3/reference-coreapi/10.4/en-us/ApiOverview/Database/RestrictionBuilder/Index.html#limitrestrictionstotables - */ $query ->getRestrictions() ->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) ; - $selectFields = array_keys($this->headers); - foreach ($selectFields as $field) { - if (isset($this->columnSelectOverrides[$field])) { - $query->addSelectLiteral(sprintf( - '%s as `%s`', - $this->columnSelectOverrides[$field], - $field, - )); - } else { - $query->addSelect($field); - } - } - // Re-apply restrictions $query ->from($this->table) @@ -155,7 +135,6 @@ protected function getTableData(array $params): array ) ; - // Apply joins $query = $this->applyJoins($query, $query); // Apply filters @@ -163,6 +142,36 @@ protected function getTableData(array $params): array $query = $this->applyFilters($query, $params); } + // Apply search + $query = $this->applySearch($query, $params); + + return $query; + } + + /** + * Get the table data + */ + protected function getTableData(array $params): array + { + $query = $this->prepareQuery($params); + + $selectFields = array_keys($this->headers); + foreach ($selectFields as $field) { + if (isset($this->columnSelectOverrides[$field])) { + $query->addSelectLiteral(sprintf( + '%s as `%s`', + $this->columnSelectOverrides[$field], + $field, + )); + } else { + $query->addSelect($field); + } + } + // Apply filters + if ($params['filters'] ?? false) { + $query = $this->applyFilters($query, $params); + } + // Page if ($params['start'] ?? false) { $query = $query->setFirstResult($params['start']); @@ -171,9 +180,6 @@ protected function getTableData(array $params): array // Order $this->applyOrder($query, $params); - // Apply search - $query = $this->applySearch($query, $params); - // Page size if ($params['length'] > 0) { $query = $query @@ -194,39 +200,9 @@ protected function getTableData(array $params): array */ protected function getCount(array $params): int { - $query = $this->getNewQueryBuilder(); - - /** - * Users without attached fees were not returned in the count due to null values - * Removing restrictions and re-apply to table only solves this - * @todo TYPO3 v10+ has a cleaner way of doing this: https://docs.typo3.org/m/typo3/reference-coreapi/10.4/en-us/ApiOverview/Database/RestrictionBuilder/Index.html#limitrestrictionstotables - */ - $query - ->getRestrictions() - ->removeAll() - ; - - $query = $query - ->count($this->table . '.uid') - ->from($this->table) - ->where( - $query->expr()->eq( - $this->table . '.deleted', - 0 - ), - ) - ; - - // Apply joins - $query = $this->applyJoins($query, $this->table); + $query = $this->prepareQuery($params); - // Apply filters - if ($params['filters']) { - $query = $this->applyFilters($query, $params); - } - - // Apply search - $query = $this->applySearch($query, $params); + $query->count($this->table . '.uid'); $count = $query ->executeQuery() From 973ccfbd20d6ab14e1675c7ceb1f398854e8055e Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Sun, 1 Sep 2024 18:36:07 +0100 Subject: [PATCH 24/37] refactor: Tidy applyOrder method --- Classes/Controller/DatatableController.php | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index a3a63c6..0b65e4e 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -358,8 +358,16 @@ public function applyOrder(QueryBuilder $query, array $params) $columnCount = count($this->headers); foreach ($orders as $order) { + + // Prepare the directions + $dir = strtoupper($order['dir'] ?? 'ASC'); + + if (!in_array($dir, ['ASC', 'DESC'], true)) { + continue; + } + + // Prepare the column index $column = $order['column'] ?? false; - $dir = $order['dir'] ?? 'ASC'; if (!is_numeric($column)) { continue; @@ -369,17 +377,11 @@ public function applyOrder(QueryBuilder $query, array $params) if (!is_int($column)) { continue; - } - - if (0 > $column || $column >= $columnCount) { - continue; - } - - if (!in_array(strtoupper($dir), ['ASC', 'DESC'], true)) { + } elseif (0 > $column || $column >= $columnCount) { continue; } - // SQL order by columns are 1-base + // Note: SQL order by column index is 1-base $query->getConcreteQueryBuilder()->addOrderBy($column + 1, $dir); } From f21e5b851e45667f7bae28ec97b8749533643d70 Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Sun, 1 Sep 2024 18:41:24 +0100 Subject: [PATCH 25/37] fix: Stop getTableData applying filters twice --- Classes/Controller/DatatableController.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index 0b65e4e..4ee8572 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -167,10 +167,6 @@ protected function getTableData(array $params): array $query->addSelect($field); } } - // Apply filters - if ($params['filters'] ?? false) { - $query = $this->applyFilters($query, $params); - } // Page if ($params['start'] ?? false) { From 6598a568a996528cb563b9ee2d4d4a22ea023104 Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Sun, 1 Sep 2024 18:44:06 +0100 Subject: [PATCH 26/37] refactor: Stop assigning query builders to themselves :facepalm: --- Classes/Controller/DatatableController.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index 4ee8572..7e4721c 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -170,7 +170,7 @@ protected function getTableData(array $params): array // Page if ($params['start'] ?? false) { - $query = $query->setFirstResult($params['start']); + $query->setFirstResult($params['start']); } // Order @@ -178,7 +178,7 @@ protected function getTableData(array $params): array // Page size if ($params['length'] > 0) { - $query = $query + $query ->setMaxResults($params['length']) ; } @@ -226,7 +226,7 @@ protected function applySearch(QueryBuilder $query, array $params): QueryBuilder ); } - $query = $query->andWhere($searchQuery); + $query->andWhere($searchQuery); } return $query; @@ -297,7 +297,7 @@ protected function applyFilters(QueryBuilder $query, array $params): QueryBuilde if (is_array($filter) && (count($filter) > 1)) { foreach ($filter as $value) { if ($field === 'usergroup') { - $query = $query + $query ->andWhere( $query->expr()->orX( $query->expr()->like( @@ -316,7 +316,7 @@ protected function applyFilters(QueryBuilder $query, array $params): QueryBuilde ) ; } else { - $query = $query + $query ->andWhere( $query->expr()->eq( $field, @@ -329,7 +329,7 @@ protected function applyFilters(QueryBuilder $query, array $params): QueryBuilde } } } else { - $query = $query + $query ->andWhere( $query->expr()->eq( $field, From 1409182ee6a9f01fc861963139d40c05db9553de Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Sun, 1 Sep 2024 19:14:55 +0100 Subject: [PATCH 27/37] style: Lint PHP --- .gitignore | 1 + .project.php_cs | 73 ++++++++++++++++++++++++++----------------------- composer.json | 9 ++++++ 3 files changed, 49 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 78d6bec..ad3c5c3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ vendor/ *GENERATED* composer.lock +.php-cs-fixer.cache diff --git a/.project.php_cs b/.project.php_cs index 3782c29..26d2023 100644 --- a/.project.php_cs +++ b/.project.php_cs @@ -12,39 +12,44 @@ $finder = PhpCsFixer\Finder::create() $config = new PhpCsFixer\Config(); return $config->setRules([ - '@PSR12' => true, - - 'indentation_type' => true, - - 'array_syntax' => ['syntax' => 'short'], - 'concat_space' => ['spacing' => 'one'], - 'no_empty_statement' => false, - 'no_whitespace_in_blank_line' => true, - - 'blank_line_after_opening_tag' => true, - 'cast_spaces' => ['space' => 'none'], - 'class_attributes_separation' => [ - 'elements' => [ - 'method' => 'one', - 'property' => 'one', - 'const' => 'one', - ], + '@PSR12' => true, + + 'indentation_type' => true, + + 'array_syntax' => ['syntax' => 'short'], + 'concat_space' => ['spacing' => 'one'], + 'no_empty_statement' => false, + 'no_whitespace_in_blank_line' => true, + + 'blank_line_after_opening_tag' => true, + 'cast_spaces' => ['space' => 'none'], + 'class_attributes_separation' => [ + 'elements' => [ + 'method' => 'one', + 'property' => 'one', + 'const' => 'one', ], - 'function_typehint_space' => true, - 'include' => true, - 'lowercase_cast' => true, - 'lowercase_static_reference' => true, - 'magic_constant_casing' => true, - 'native_function_casing' => true, - 'no_alternative_syntax' => true, - 'ternary_operator_spaces' => true, - 'unary_operator_spaces' => true, - - 'multiline_whitespace_before_semicolons' => [ - 'strategy' => 'new_line_for_chained_calls', - ], - 'trailing_comma_in_multiline' => true, - ]) - ->setIndent(' ') + ], + 'type_declaration_spaces' => true, + 'include' => true, + 'lowercase_cast' => true, + 'lowercase_static_reference' => true, + 'magic_constant_casing' => true, + 'native_function_casing' => true, + 'no_alternative_syntax' => true, + 'ternary_operator_spaces' => true, + 'unary_operator_spaces' => true, + 'array_indentation' => true, + 'statement_indentation' => true, + + 'multiline_whitespace_before_semicolons' => [ + 'strategy' => 'new_line_for_chained_calls', + ], + 'trailing_comma_in_multiline' => true, + 'no_multiline_whitespace_around_double_arrow' => true, + 'single_space_around_construct' => true, +]) + ->setIndent("\t") ->setFinder($finder) - ; + ->setParallelConfig(\PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) +; diff --git a/composer.json b/composer.json index 2465d50..4214681 100644 --- a/composer.json +++ b/composer.json @@ -51,6 +51,15 @@ "rm -rf .github", "rm .gitignore", "rm .project.php_cs" + ], + "lint": [ + "@lint:php" + ], + "lint:php": [ + "php-cs-fixer fix . --config=.project.php_cs" ] + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.64" } } From 1fa7c811cfd4263dfe5918888e857be9d85dd23f Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Sun, 1 Sep 2024 19:21:53 +0100 Subject: [PATCH 28/37] build: Format upcoming --- UPCOMING.md | 1 - 1 file changed, 1 deletion(-) diff --git a/UPCOMING.md b/UPCOMING.md index 9f64382..d4f3bfd 100644 --- a/UPCOMING.md +++ b/UPCOMING.md @@ -4,5 +4,4 @@ - BREAKING CHANGE: Restructure SetupTS related to `module.tx_moduledatalisting` (see README.md) - BREAKING CHANGE: `DatatableController` has various changes to properties and methods (see README.md) -- BREAKING CHANGE: Move Typoscript to Configuration to avoid auto loading (see README.md) - Enabled dependency injection auto-wiring in Services.yaml From 1a9bb4353af78ecc990f5a533df994edca060034 Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Sun, 1 Sep 2024 19:35:33 +0100 Subject: [PATCH 29/37] fix: Restore missing FROM in SQL statements --- Classes/Controller/DatatableController.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index 7e4721c..df22dce 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -118,6 +118,8 @@ protected function prepareQuery(array $params): QueryBuilder { $query = $this->getNewQueryBuilder(); + $query->from($this->table); + $query ->getRestrictions() ->removeAll() From 39172c8c5bc703aca411039ef2a5aa9370ecc148 Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Sun, 1 Sep 2024 19:36:22 +0100 Subject: [PATCH 30/37] refactor: Unify and safeguard deletion filtering --- Classes/Controller/DatatableController.php | 35 ++++++++++------------ 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index df22dce..e45512b 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -127,15 +127,7 @@ protected function prepareQuery(array $params): QueryBuilder ; // Re-apply restrictions - $query - ->from($this->table) - ->where( - $query->expr()->eq( - $this->table . '.deleted', - 0 - ), - ) - ; + $this->applyDeleteFilter($query, $this->table, $this->table); $query = $this->applyJoins($query, $query); @@ -272,21 +264,26 @@ protected function applyJoins(QueryBuilder $query): QueryBuilder // Perform the join $query->$type($this->table, $table, $alias, $on); - // Exclude anything that is deleted - if ($deleteFiled = $GLOBALS['TCA'][$table]['ctrl']['delete'] ?? false) { - $deleteFiled = $alias . '.' . $deleteFiled; - $query->where( - $query->expr()->orX( - $query->expr()->eq($deleteFiled, 0), - $query->expr()->isNull($deleteFiled), - ), - ); - } + $this->applyDeleteFilter($query, $table, $alias); } return $query; } + protected function applyDeleteFilter(QueryBuilder $query, string $table, string $alias, bool $restrict = true) + { + // Exclude anything that is deleted + if ($deleteFiled = $GLOBALS['TCA'][$table]['ctrl']['delete'] ?? false) { + $deleteFiled = $alias . '.' . $deleteFiled; + $query->where( + $query->expr()->orX( + $query->expr()->eq($deleteFiled, 0), + $query->expr()->isNull($deleteFiled), + ), + ); + } + } + /** * Apply filters to query */ From cef631dde1f2f951273e69a1adab4f5f0ac3de67 Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Mon, 2 Sep 2024 23:12:07 +0100 Subject: [PATCH 31/37] refactor: Datatable apply* methods return self --- Classes/Controller/DatatableController.php | 33 ++++++++++------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index e45512b..a742ec1 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -127,17 +127,12 @@ protected function prepareQuery(array $params): QueryBuilder ; // Re-apply restrictions - $this->applyDeleteFilter($query, $this->table, $this->table); - - $query = $this->applyJoins($query, $query); - - // Apply filters - if ($params['filters'] ?? false) { - $query = $this->applyFilters($query, $params); - } - - // Apply search - $query = $this->applySearch($query, $params); + $this + ->applyDeleteFilter($query, $this->table, $this->table) + ->applyJoins($query, $query) + ->applyFilters($query, $params) + ->applySearch($query, $params) + ; return $query; } @@ -205,7 +200,7 @@ protected function getCount(array $params): int /** * Apply search to query */ - protected function applySearch(QueryBuilder $query, array $params): QueryBuilder + protected function applySearch(QueryBuilder $query, array $params): self { if ($params['search']['value']) { $searchableColumns = GeneralUtility::trimExplode(',', $this->searchableColumns); @@ -223,13 +218,13 @@ protected function applySearch(QueryBuilder $query, array $params): QueryBuilder $query->andWhere($searchQuery); } - return $query; + return $this; } /** * Apply joins to query */ - protected function applyJoins(QueryBuilder $query): QueryBuilder + protected function applyJoins(QueryBuilder $query): self { $joins = $this->joins ?? []; @@ -267,10 +262,10 @@ protected function applyJoins(QueryBuilder $query): QueryBuilder $this->applyDeleteFilter($query, $table, $alias); } - return $query; + return $this; } - protected function applyDeleteFilter(QueryBuilder $query, string $table, string $alias, bool $restrict = true) + protected function applyDeleteFilter(QueryBuilder $query, string $table, string $alias, bool $restrict = true): self { // Exclude anything that is deleted if ($deleteFiled = $GLOBALS['TCA'][$table]['ctrl']['delete'] ?? false) { @@ -282,12 +277,14 @@ protected function applyDeleteFilter(QueryBuilder $query, string $table, string ), ); } + + return $this; } /** * Apply filters to query */ - protected function applyFilters(QueryBuilder $query, array $params): QueryBuilder + protected function applyFilters(QueryBuilder $query, array $params): self { foreach ($params['filters'] ?? [] as $field => $filter) { // If filtering by usergroup @@ -340,7 +337,7 @@ protected function applyFilters(QueryBuilder $query, array $params): QueryBuilde ; } } - return $query; + return $this; } /** From dcd6408277c94d1b88f44949203a00242b12813b Mon Sep 17 00:00:00 2001 From: Mike Street Date: Wed, 4 Sep 2024 20:30:33 +0100 Subject: [PATCH 32/37] fix(controller): Ensure field isset before accessing --- Classes/Controller/FeUsersController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Controller/FeUsersController.php b/Classes/Controller/FeUsersController.php index a37827b..aa7d67b 100644 --- a/Classes/Controller/FeUsersController.php +++ b/Classes/Controller/FeUsersController.php @@ -129,7 +129,7 @@ function ($usergroupUid): ?string { // Format unix timestamp fields foreach ($this->dateColumns as $dateColumn) { - $row[$dateColumn] = $row[$dateColumn] ? date('d/m/Y H:i:s', $row[$dateColumn]) : 'N/A'; + $row[$dateColumn] = (isset($row[$dateColumn]) && $row[$dateColumn]) ? date('d/m/Y H:i:s', $row[$dateColumn]) : 'N/A'; } $data[] = array_values($row); From 79052b31d6c9b2c3c74f4914cef5219236d0cf0e Mon Sep 17 00:00:00 2001 From: Mike Street Date: Wed, 4 Sep 2024 20:32:06 +0100 Subject: [PATCH 33/37] style: Lint code --- Classes/Controller/FeUsersController.php | 6 +++++- composer.json | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Classes/Controller/FeUsersController.php b/Classes/Controller/FeUsersController.php index aa7d67b..1f4ae6d 100644 --- a/Classes/Controller/FeUsersController.php +++ b/Classes/Controller/FeUsersController.php @@ -129,7 +129,11 @@ function ($usergroupUid): ?string { // Format unix timestamp fields foreach ($this->dateColumns as $dateColumn) { - $row[$dateColumn] = (isset($row[$dateColumn]) && $row[$dateColumn]) ? date('d/m/Y H:i:s', $row[$dateColumn]) : 'N/A'; + if (!isset($row[$dateColumn])) { + continue; + } + + $row[$dateColumn] = $row[$dateColumn] ? date('d/m/Y H:i:s', $row[$dateColumn]) : 'N/A'; } $data[] = array_values($row); diff --git a/composer.json b/composer.json index 4214681..a972183 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,9 @@ "typo3/cms-core": "^11.5", "typo3/cms-extbase": "^11.5" }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.64" + }, "autoload": { "psr-4": { "LiquidLight\\ModuleDataListing\\": "Classes" @@ -58,8 +61,5 @@ "lint:php": [ "php-cs-fixer fix . --config=.project.php_cs" ] - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.64" } } From 48f3d154a00425c9b17713fd6f36f29b9f0f5d2f Mon Sep 17 00:00:00 2001 From: Mike Street Date: Tue, 1 Oct 2024 16:12:18 +0100 Subject: [PATCH 34/37] refactor: Move typoscript to folder This avoids autoloading later in the stack --- Configuration/TCA/Overrides/sys_template.php | 11 +++++++++++ .../TypoScript/setup.typoscript | 0 2 files changed, 11 insertions(+) create mode 100644 Configuration/TCA/Overrides/sys_template.php rename ext_typoscript_setup.typoscript => Configuration/TypoScript/setup.typoscript (100%) diff --git a/Configuration/TCA/Overrides/sys_template.php b/Configuration/TCA/Overrides/sys_template.php new file mode 100644 index 0000000..35b804b --- /dev/null +++ b/Configuration/TCA/Overrides/sys_template.php @@ -0,0 +1,11 @@ + Date: Tue, 1 Oct 2024 21:14:09 +0100 Subject: [PATCH 35/37] feat(javascript): Add extendable javascript module --- .../Public/JavaScript/ModuleDataListing.js | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 Resources/Public/JavaScript/ModuleDataListing.js diff --git a/Resources/Public/JavaScript/ModuleDataListing.js b/Resources/Public/JavaScript/ModuleDataListing.js new file mode 100644 index 0000000..2e898b8 --- /dev/null +++ b/Resources/Public/JavaScript/ModuleDataListing.js @@ -0,0 +1,147 @@ +/* global define, TYPO3 */ +/* eslint no-unused-vars: "off" */ + +define([ + 'jquery', + 'datatables.net', + 'datatables.net-buttons', + 'datatables.net-buttons-print', + 'datatables.net-buttons-html5' +], function () { + var ModuleDataListing = { + settings: { + ajaxUrlKey: '', + tableSelector: '#mdl-datatable', + searchButtonSelector: '.mdl-search', + + dataTable: { + processing: true, + serverSide: true, + order: [[0, 'desc']], + layout: { + bottom2: 'buttons', + }, + buttons: [ + { + extend: 'csv', + text: 'Download as CSV', + className: 'btn btn-primary' + }, + { + extend: 'copy', + text: 'Copy to clipboard', + className: 'btn btn-default' + } + ], + pagingType: 'full_numbers', + language: { + emptyTable: 'No data available in table' + }, + lengthMenu: [ + [ + 10, + 25, + 50, + 100, + -1 + ], + [ + 10, + 25, + 50, + 100, + 'All' + ] + ], + classes: { + sLength: 'form-group', + sLengthSelect: 'form-control input-sm', + sFilter: 'form-group', + sFilterInput: 'form-control input-sm' + } + } + }, + + config: function(options = {}) { + ModuleDataListing.settings = Object.assign({}, ModuleDataListing.settings, options); + } + }; + + ModuleDataListing.local = { + get: function(key) { + let storage = localStorage.getItem('ModuleDataListing'); + storage = JSON.parse(storage); + + return storage[key]; + }, + set: function(key, data) { + let storage = localStorage.getItem('ModuleDataListing') ?? ''; + storage = JSON.parse(storage); + + storage[key] = data; + + localStorage.setItem('ModuleDataListing', JSON.stringify(storage)); + }, + } + + ModuleDataListing.dataTable = { + init: function(filters) { + const settings = { + search: { + search: '' + }, + ajax: { + url: TYPO3.settings.ajaxUrls[ModuleDataListing.settings.ajaxUrlKey], + data: { + filters: filters + } + }, + } + const table = $(ModuleDataListing.settings.tableSelector) + .DataTable( + Object.assign({}, ModuleDataListing.settings.dataTable, settings) + ); + + table.on('search.dt', function () { + // // Get the current URL + // let url = new URL(window.location); + // url.searchParams.set('search', table.search()); + // window.history.pushState({}, '', url); + }); + + return table; + }, + + destroy: function() { + $(ModuleDataListing.settings.tableSelector).DataTable().destroy(); + }, + + restart: function(filters) { + ModuleDataListing.dataTable.destroy(); + ModuleDataListing.dataTable.init(filters); + } + }; + + ModuleDataListing.filters = { + init: function () { + $(ModuleDataListing.settings.searchButtonSelector).click(function () { + let filters = {} + + $('div[data-mdl-filter]').each(function () { + let self = $(this); + let filterData = []; + + self.find('input:checked').each(function () { + filterData.push($(this).val()); + }); + + filters[self.data('mdl-filter')] = filterData; + }); + + ModuleDataListing.dataTable.restart(filters); + }); + } + } + + return ModuleDataListing; +}); From 4b8b2b50a3b356690745466dd9f94e706c585cae Mon Sep 17 00:00:00 2001 From: Mike Street Date: Tue, 1 Oct 2024 22:34:22 +0100 Subject: [PATCH 36/37] feat(javascript): Add choice of remebering filters Also add comments --- .../Public/JavaScript/ModuleDataListing.js | 192 +++++++++++++++--- 1 file changed, 163 insertions(+), 29 deletions(-) diff --git a/Resources/Public/JavaScript/ModuleDataListing.js b/Resources/Public/JavaScript/ModuleDataListing.js index 2e898b8..6270645 100644 --- a/Resources/Public/JavaScript/ModuleDataListing.js +++ b/Resources/Public/JavaScript/ModuleDataListing.js @@ -10,10 +10,18 @@ define([ ], function () { var ModuleDataListing = { settings: { - ajaxUrlKey: '', + // The URL called when updating the datatable + ajaxUrl: '', + // If set, localStorage is used for rembering filters + storageKey: null, + // jQuery selector of your datatable tableSelector: '#mdl-datatable', - searchButtonSelector: '.mdl-search', + // jQuery selector of button for filtering + filterButtonSelector: '.mdl-filter-search', + // jQuery selector of button for clearing filters + clearButtonSelector: '.mdl-filter-clear', + // DataTable options dataTable: { processing: true, serverSide: true, @@ -62,84 +70,210 @@ define([ } }, + // Setting config - merges options config: function(options = {}) { - ModuleDataListing.settings = Object.assign({}, ModuleDataListing.settings, options); + ModuleDataListing.settings = ModuleDataListing.utility.mergeObjects(ModuleDataListing.settings, options); } }; - ModuleDataListing.local = { + /** + * Local Storage + * + * Only works if `storageKey` is set. + * + * Everything is stored in a single JSON object keyed by + * ModuleDataListing.${settings.storageKey} + */ + ModuleDataListing.storage = { + + /** + * Get an item from localStorage + */ get: function(key) { - let storage = localStorage.getItem('ModuleDataListing'); - storage = JSON.parse(storage); + if (!ModuleDataListing.settings.storageKey) { + return null; + } + + let storage = localStorage.getItem( + 'ModuleDataListing.' + ModuleDataListing.settings.storageKey + ); + storage = storage ? JSON.parse(storage) : {}; - return storage[key]; + return storage.hasOwnProperty(key) ? storage[key] : null; }, + + /** + * Add an item to localStorage + */ set: function(key, data) { - let storage = localStorage.getItem('ModuleDataListing') ?? ''; - storage = JSON.parse(storage); + if (!ModuleDataListing.settings.storageKey) { + return; + } + // Load existing storage + let storage = localStorage.getItem( + 'ModuleDataListing.' + ModuleDataListing.settings.storageKey + ); + + storage = storage ? JSON.parse(storage) : {}; + + // Add our new data storage[key] = data; - localStorage.setItem('ModuleDataListing', JSON.stringify(storage)); + // Store it + localStorage.setItem( + 'ModuleDataListing.' + ModuleDataListing.settings.storageKey, + JSON.stringify(storage) + ); }, + + /** + * Empty the localsotrage for this storage key + */ + clear: function() { + localStorage.removeItem( + 'ModuleDataListing.' + ModuleDataListing.settings.storageKey + ) + } } + /** + * DataTable functions + * + * Uses https://datatables.net/ + */ ModuleDataListing.dataTable = { - init: function(filters) { + init: function(filters = null) { + + // See if we can get filters from storage + if(!filters) { + filters = ModuleDataListing.storage.get('filters'); + } + + // Add the dynamic options const settings = { search: { - search: '' + search: ModuleDataListing.storage.get('keywords') }, + ajax: { - url: TYPO3.settings.ajaxUrls[ModuleDataListing.settings.ajaxUrlKey], + url: ModuleDataListing.settings.ajaxUrl, data: { filters: filters } }, } + + // Initialise the datatable const table = $(ModuleDataListing.settings.tableSelector) .DataTable( - Object.assign({}, ModuleDataListing.settings.dataTable, settings) + ModuleDataListing.utility.mergeObjects(ModuleDataListing.settings.dataTable, settings) ); + // Add listener for search box to update storage table.on('search.dt', function () { - // // Get the current URL - // let url = new URL(window.location); - // url.searchParams.set('search', table.search()); - // window.history.pushState({}, '', url); + ModuleDataListing.storage.set('keywords', table.search()); }); return table; }, + // Remove datatable config destroy: function() { $(ModuleDataListing.settings.tableSelector).DataTable().destroy(); }, - restart: function(filters) { + restart: function(filters = null) { ModuleDataListing.dataTable.destroy(); ModuleDataListing.dataTable.init(filters); } }; + /** + * Filter + */ ModuleDataListing.filters = { + // Filter setup init: function () { - $(ModuleDataListing.settings.searchButtonSelector).click(function () { - let filters = {} - $('div[data-mdl-filter]').each(function () { - let self = $(this); - let filterData = []; + // Set any filters from storage + ModuleDataListing.filters.set( + ModuleDataListing.storage.get('filters') + ); + + // Add click listener to search button + $(ModuleDataListing.settings.filterButtonSelector).click(function () { + let filters = ModuleDataListing.filters.get(); + ModuleDataListing.storage.set('filters', filters); + ModuleDataListing.dataTable.restart(filters); + }); + + // Remove all filters when cleared + $(ModuleDataListing.settings.clearButtonSelector).click(function () { + ModuleDataListing.storage.clear(); + ModuleDataListing.filters.clear(); + ModuleDataListing.dataTable.restart({}); + }); + }, + + /** + * Get an object & array of checked items + */ + get: function () { + let filters = {}; - self.find('input:checked').each(function () { - filterData.push($(this).val()); - }); + $('div[data-mdl-filter]').each(function () { + let self = $(this); + let filterData = []; - filters[self.data('mdl-filter')] = filterData; + self.find('input:checked').each(function () { + filterData.push($(this).val()); }); - ModuleDataListing.dataTable.restart(filters); + filters[self.data('mdl-filter')] = filterData; }); + + return filters; + }, + + /** + * Check any checkbox which were in the filters object + */ + set: function (filters = {}) { + for (let filter in filters) { + let container = $(`div[data-mdl-filter="${filter}"]`); + for (let value of filters[filter]) { + container.find(`input[value="${value}"]`).prop('checked', true); + } + } + }, + + /** + * Uncheck all checkboxes + */ + clear: function () { + $('div[data-mdl-filter] input:checked').each(function() { + $(this).prop('checked', false); + }) + }, + } + + ModuleDataListing.utility = { + // Filter setup + mergeObjects: function (obj1, obj2) { + const result = JSON.parse(JSON.stringify(obj1)); + + for (let key in obj2) { + if (obj2[key] instanceof Array && obj1[key] instanceof Array) { + result[key] = obj2[key]; + } else if (obj2[key] instanceof Object && obj1[key] instanceof Object) { + result[key] = ModuleDataListing.utility.mergeObjects(obj1[key], obj2[key]); + } else { + result[key] = obj2[key]; + } + } + + return result; } } From c331869c1952f7283d2b761faec78e3093523d91 Mon Sep 17 00:00:00 2001 From: Garett Robson Date: Wed, 26 Mar 2025 10:47:48 +0000 Subject: [PATCH 37/37] fix: Searching overridden fields uses the override --- Classes/Controller/DatatableController.php | 16 ++++++++++------ README.md | 5 ++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Classes/Controller/DatatableController.php b/Classes/Controller/DatatableController.php index a742ec1..6c5987b 100644 --- a/Classes/Controller/DatatableController.php +++ b/Classes/Controller/DatatableController.php @@ -207,12 +207,16 @@ protected function applySearch(QueryBuilder $query, array $params): self $searchQuery = $query->expr()->orX(); foreach ($searchableColumns as $field) { - $searchQuery->add( - $query->expr()->like( - $field, - $query->createNamedParameter('%' . $query->escapeLikeWildcards($params['search']['value']) . '%') - ) - ); + $param = $query->createNamedParameter('%' . $query->escapeLikeWildcards($params['search']['value']) . '%'); + + $expression = isset($this->columnSelectOverrides[$field]) ? + // If we have a column override we need to filter on that + // override and not the field (alias) itself + sprintf('%s LIKE %s', $this->columnSelectOverrides[$field], $param) : + // Otherwise we can filter directly off the field itself + $query->expr()->like($field, $param); + + $searchQuery->add($expression); } $query->andWhere($searchQuery); diff --git a/README.md b/README.md index a6a3d62..8b48bf6 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ There number of critical differences between v1 and v2. * New property `protected array $columnSelectOverrides` maps fields to complex SQL; useful for handling computed values. * Method `protected function getConnection(string $table): Connection` changed to `protected function getConnection(?string $table = null): Connection`. Calling without an argument uses `$this->table`. * Method `protected function getHeaders(array $default): array` changed to `protected function getHeaders(): array`. Uses `$this->headers` internally, which was otherwise always passed-in. -* Method `indexAction(): void` implemented as per the old sub-class instructions. As a result you no longer need to define `indexAction()` to have default behaviour, you can alternatively call `parent::index()` to expand on the default behaviour. +* Method `indexAction(): void` implemented as per the old sub-class instructions. As a result you no longer need to define `indexAction()` to have default behaviour, you can alternatively call `parent::indexAction()` to expand on the default behaviour. > [!Note] > Previously the `$table`, `$moduleName` and `$headers` properties where not _explicitly_ defined, but where expected to be defined in sub-classes. They are now explicitly defined in this class. If you have previously extended `DatatableController` you will likely need to change your definitions to match. @@ -196,3 +196,6 @@ The following in a breakdown of the class properties and their respective typosc > [!Note] > When joining tables you should use the alias in place of the table name for the purposes of `searchableColumns`, `headers`, and `columnSelectOverrides`. + +> [!Note] +> When performing a search on any fields that are defined in `$columnSelectOverrides`, the WHERE condition will include the overridden SQL. This prevents an alias from being used in the case of a complex SQL expression.