From d4a006e3526604053f9b9ea6004fce8e0fa506a7 Mon Sep 17 00:00:00 2001 From: Nikita Hovratov Date: Fri, 6 Jun 2025 13:57:06 +0200 Subject: [PATCH] [FEATURE] Allow to restrict number of items by CType Fixes: #170 --- .../Form/FormDataProvider/TcaColPosItems.php | 15 ++++++++++++ Classes/Hooks/AbstractDataHandlerHook.php | 18 ++++++++++++++ Classes/Hooks/DatamapDataHandlerHook.php | 24 +++++++++++++++++++ Classes/Repository/ContentRepository.php | 20 ++++++++++++++-- README.md | 19 +++++++++++++++ 5 files changed, 94 insertions(+), 2 deletions(-) diff --git a/Classes/Form/FormDataProvider/TcaColPosItems.php b/Classes/Form/FormDataProvider/TcaColPosItems.php index 61d6587..936442b 100644 --- a/Classes/Form/FormDataProvider/TcaColPosItems.php +++ b/Classes/Form/FormDataProvider/TcaColPosItems.php @@ -94,6 +94,21 @@ public function addData(array $result) } } + $cType = $record['CType'][0]; + if (!empty($columnConfiguration['maxitemsByCType.'][$cType]) + && $columnConfiguration['maxitemsByCType.'][$cType] <= $this->contentRepository->countColPosByRecordType($record, $cType) + ) { + $isCurrentColPos = $colPos === (int)$result['databaseRow']['colPos'][0]; + if ($isCurrentColPos && !$this->contentRepository->isRecordInColPos($record)) { + throw new AccessDeniedColPosException( + 'Maximum number of allowed content elements by CType "' . $cType . '" (' . $columnConfiguration['maxitemsByCType.'][$cType] . ') reached.', + 1749209557 + ); + } elseif (!$isCurrentColPos) { + unset($result['processedTca']['columns']['colPos']['config']['items'][$key]); + } + } + if (!empty($columnConfiguration['maxitems']) && $columnConfiguration['maxitems'] <= $this->contentRepository->countColPosByRecord($record) ) { diff --git a/Classes/Hooks/AbstractDataHandlerHook.php b/Classes/Hooks/AbstractDataHandlerHook.php index 9315b39..99b2753 100644 --- a/Classes/Hooks/AbstractDataHandlerHook.php +++ b/Classes/Hooks/AbstractDataHandlerHook.php @@ -138,4 +138,22 @@ protected function isRecordAllowedByItemsCount(array $columnConfiguration, array return (int)$columnConfiguration['maxitems'] >= $this->contentRepository->addRecordToColPos($record); } + + /** + * @param array $columnConfiguration + * @param array $record + * @return bool + */ + protected function isRecordAllowedByItemsCountAndCType(array $columnConfiguration, array $record) + { + $cType = $record['CType']; + if (!empty($columnConfiguration['maxitemsByCType.'])) { + $this->contentRepository->addRecordToColPos($record); + if ($this->contentRepository->countColPosByRecordType($record, $cType) >= (int)$columnConfiguration['maxitemsByCType.']) { + return false; + } + } + + return true; + } } diff --git a/Classes/Hooks/DatamapDataHandlerHook.php b/Classes/Hooks/DatamapDataHandlerHook.php index 67f5247..4044b5e 100644 --- a/Classes/Hooks/DatamapDataHandlerHook.php +++ b/Classes/Hooks/DatamapDataHandlerHook.php @@ -70,6 +70,30 @@ public function processDatamap_beforeStart(DataHandler $dataHandler) ); } + if (!$this->isRecordAllowedByItemsCountAndCType($columnConfiguration, $incomingFieldArray)) { + // DataHandler copies a record by first add a new content element (in the old colPos) and then adjust + // the colPos information to the target colPos. This means we have to allow this element to be added + // even if the maxitems is reached already. The copy command was checked in CmdmapDataHandlerHook. + if (empty($dataHandler->cmdmap) && !empty($_POST['CB']['paste'] ?? $_GET['CB']['paste'] ?? null)) { + continue; + } + $cType = $incomingFieldArray['CType']; + unset($dataHandler->datamap['tt_content'][$id]); + $dataHandler->log( + 'tt_content', + $id, + 1, + $pageId, + 1, + 'The record "%s" couldn\'t be saved due to reached maxitemsByCType configuration of %d.', + 27, + [ + $incomingFieldArray[$GLOBALS['TCA']['tt_content']['ctrl']['label']], + $columnConfiguration['maxitemsByCType.'][$cType], + ] + ); + } + if (!$this->isRecordAllowedByItemsCount($columnConfiguration, $incomingFieldArray)) { // DataHandler copies a record by first add a new content element (in the old colPos) and then adjust // the colPos information to the target colPos. This means we have to allow this element to be added diff --git a/Classes/Repository/ContentRepository.php b/Classes/Repository/ContentRepository.php index 63f91be..51e9f27 100644 --- a/Classes/Repository/ContentRepository.php +++ b/Classes/Repository/ContentRepository.php @@ -46,6 +46,21 @@ public function countColPosByRecord(array $record): int return count($this->colPosCount[$identifier]); } + public function countColPosByRecordType(array $record, string $cType): int + { + $identifier = $this->getIdentifier($record); + + if (!isset($this->colPosCount[$identifier])) { + $this->initialize($record); + } + + $colPosCountState = $this->colPosCount[$identifier]; + $allEntries = $colPosCountState[null]; + $countByCType = array_count_values($allEntries); + + return $countByCType[$cType] ?? 0; + } + public function addRecordToColPos(array $record): int { $identifier = $this->getIdentifier($record); @@ -118,7 +133,7 @@ protected function fetchRecordsForColPos(array $record): array $languageField = $GLOBALS['TCA']['tt_content']['ctrl']['languageField']; $language = (array)($record[$languageField] ?? 0); - $selectFields = ['uid', 'pid']; + $selectFields = ['uid', 'pid', 'CType']; if (!empty($GLOBALS['TCA']['tt_content']['ctrl']['versioningWS'])) { $selectFields[] = 't3ver_state'; } @@ -151,7 +166,8 @@ protected function fetchRecordsForColPos(array $record): array BackendUtility::workspaceOL('tt_content', $row, -99, true); if (is_array($row) && !VersionState::cast($row['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) { $uid = ($row['_ORIG_uid'] ?? 0) ?: $row['uid']; - $rows[$uid] = $uid; + $cType = $row['CType']; + $rows[$uid] = $cType; } } diff --git a/README.md b/README.md index 2ed435c..390cf12 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,25 @@ columns { } ``` +- To restrict the number of content elements by type use `maxitemsByCType.[type] = [number of elements]` + +*Example:* +``` +columns { + 1 { + name = Column with one textmedia + colPos = 3 + colspan = 6 + allowed { + CType = textmedia, header + } + maxitemsByCType { + header = 1 + } + } +} +``` + ## Known issues ### TypeError