diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3e3a70c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,57 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +# TS/JS-Files +[*.{ts,js,vue}] +indent_size = 4 +indent_style = tab + +# JSON-Files +[*.json] +indent_style = tab + +# ReST-Files +[*.rst] +indent_size = 4 +max_line_length = 80 + +# YAML-Files +[*.{yaml,yml}] +indent_size = 2 + +# NEON-Files +[*.neon] +indent_size = 2 +indent_style = tab + +# package.json +[package.json] +indent_size = 2 + +# TypoScript +[*.{typoscript,tsconfig}] +indent_size = 2 + +# XLF-Files +[*.xlf] +indent_style = tab + +# SQL-Files +[*.sql] +indent_style = tab +indent_size = 2 + +# .htaccess +[{_.htaccess,.htaccess}] +indent_style = tab diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..85809a0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +/.editorconfig export-ignore +/.gitignore export-ignore +/.gitattributes export-ignore +/.php-cs-fixer.php export-ignore +/.phpstan.neon export-ignore diff --git a/.gitignore b/.gitignore index a1ee133..04fc4e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -/build/ /composer.lock -/.php_cs.cache -/public/ \ No newline at end of file +/vendor/ +/public/ diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..df7b827 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,10 @@ +in('Classes') + ->in('Configuration'); + +$config = \TYPO3\CodingStandards\CsFixerConfig::create(); +return $config + ->setUsingCache(false) + ->setFinder($finder); diff --git a/Classes/Controller/SuggestReceiver.php b/Classes/Controller/SuggestReceiver.php index aef84f9..57fb342 100644 --- a/Classes/Controller/SuggestReceiver.php +++ b/Classes/Controller/SuggestReceiver.php @@ -1,5 +1,7 @@ tagRepository = GeneralUtility::makeInstance(TagRepository::class); + $this->tagRepository = $tagRepository; } public function findSuitableTags(ServerRequestInterface $request): ResponseInterface @@ -39,6 +34,7 @@ public function findSuitableTags(ServerRequestInterface $request): ResponseInter } else { $tags = []; } + return new JsonResponse($tags); } -} \ No newline at end of file +} diff --git a/Classes/Domain/Repository/TagRepository.php b/Classes/Domain/Repository/TagRepository.php index e270e0c..66bff6f 100644 --- a/Classes/Domain/Repository/TagRepository.php +++ b/Classes/Domain/Repository/TagRepository.php @@ -1,5 +1,7 @@ connectionPool = $connectionPool; + } public function findRecordsForTagNames(array $tagNames): array { - $queryBuilder = $this->getConnection()->createQueryBuilder(); + $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME); $stmt = $queryBuilder ->select('*') - ->from($this->tableName) + ->from(self::TABLE_NAME) ->where( - $queryBuilder->expr()->in( - 'name', - $queryBuilder->createNamedParameter($tagNames, Connection::PARAM_STR_ARRAY) - ) + $queryBuilder->expr()->in('name', $queryBuilder->createNamedParameter($tagNames, Connection::PARAM_STR_ARRAY)) ) ->executeQuery(); @@ -39,33 +44,36 @@ public function findRecordsForTagNames(array $tagNames): array while ($row = $stmt->fetchAssociative()) { $mappedItems[$row['name']] = $row['uid']; } + return $mappedItems; } - public function add(string $tagName, int $pid = 0) + public function add(string $tagName, int $pid = 0): string { - $conn = $this->getConnection(); - $conn->insert($this->tableName, ['name' => $tagName, 'pid' => $pid, 'createdon' => $GLOBALS['EXEC_TIME']]); + $conn = $this->connectionPool->getConnectionForTable(self::TABLE_NAME); + $conn->insert( + self::TABLE_NAME, + [ + 'name' => $tagName, + 'pid' => $pid, + 'createdon' => $GLOBALS['EXEC_TIME'], + ] + ); + return $conn->lastInsertId(); } /** * Simple query for looking for tags that contain the search word. No multi-word / and/or search implemented yet. - * - * @param string $searchWord - * @return array */ public function search(string $searchWord): array { - $queryBuilder = $this->getConnection()->createQueryBuilder(); + $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME); $stmt = $queryBuilder ->select('*') - ->from($this->tableName) + ->from(self::TABLE_NAME) ->where( - $queryBuilder->expr()->like( - 'name', - $queryBuilder->createNamedParameter('%' . $queryBuilder->escapeLikeWildcards($searchWord) . '%') - ) + $queryBuilder->expr()->like('name', $queryBuilder->createNamedParameter('%' . $queryBuilder->escapeLikeWildcards($searchWord) . '%')) ) ->executeQuery(); @@ -73,14 +81,9 @@ public function search(string $searchWord): array while ($row = $stmt->fetchAssociative()) { $items[] = [ 'value' => (int)$row['uid'], - 'name' => $row['name'] + 'name' => $row['name'], ]; } return $items; } - - private function getConnection(): Connection - { - return GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->tableName); - } } diff --git a/Classes/Form/TagListElement.php b/Classes/Form/TagListElement.php index ccb30cd..a31696f 100644 --- a/Classes/Form/TagListElement.php +++ b/Classes/Form/TagListElement.php @@ -1,5 +1,7 @@ uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + $this->nodeFactory = GeneralUtility::makeInstance(NodeFactory::class); + } + + public function render(): array { $resultArray = $this->initializeResultArray(); $selectedItems = $this->data['parameterArray']['itemFormElValue'] ?? []; @@ -86,37 +98,17 @@ public function render() ]; } - $ajaxUrl = GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute('ajax_tag_suggest_tags'); + $ajaxUrl = $this->uriBuilder->buildUriFromRoute('ajax_tag_suggest_tags'); $resultArray['html'] = implode(LF, $html); $resultArray['stylesheetFiles'][] = 'EXT:tag/Resources/Public/StyleSheets/tagsinput.css'; - if ((new Typo3Version())->getMajorVersion() < 12) { - $resultArray['requireJsModules'][] = [ - 'TYPO3/CMS/Tag/TagsInputElement' => 'function(TagsInputElement) { - new TagsInputElement("' . $tagsId . '", { - itemValue: function(item) { - return item.value || item; - }, - itemText: function(item) { - return item.name || item; - }, - items: ' . json_encode($items) . ', - typeahead: { - minLength: 2, - source: function(query) { - var url = ' . GeneralUtility::quoteJSvalue($ajaxUrl) . ' + "&q=" + query; - return $.getJSON(url); - } - } - }); - }' - ]; - } else { - $resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create( - '@b13/tag/tags-input-element.js', - )->instance($tagsId, $items, (string)$ajaxUrl); - } + $resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@b13/tag/tags-input-element.js')->instance($tagsId, $items, (string)$ajaxUrl); return $resultArray; } + + public function setData(array $data): void + { + $this->data = $data; + } } diff --git a/Classes/Persistence/PrepareTagItems.php b/Classes/Persistence/PrepareTagItems.php index 28d9ee4..890580a 100644 --- a/Classes/Persistence/PrepareTagItems.php +++ b/Classes/Persistence/PrepareTagItems.php @@ -1,5 +1,7 @@ tagRepository = GeneralUtility::makeInstance(TagRepository::class); + $this->tagRepository = $tagRepository; } /** * DataHandler hook to create tags automatically if they don't exist yet. This way, a clean list of * IDs is entered to DataHandler. - * - * @param $incomingFieldArray - * @param $table - * @param $id - * @param DataHandler $dataHandler */ - public function processDatamap_preProcessFieldArray(&$incomingFieldArray, $table, $id, DataHandler $dataHandler) + public function processDatamap_preProcessFieldArray(array &$incomingFieldArray, string $table, string $id, DataHandler $dataHandler) { $relevantFields = (new TcaHelper())->findTagFieldsForTable($table); if (empty($relevantFields)) { @@ -77,10 +72,6 @@ public function processDatamap_preProcessFieldArray(&$incomingFieldArray, $table /** * See what tags are already in the database and add missing tags, and map the tag names to the IDs. - * - * @param array $tags - * @param int $pid - * @return array */ protected function normalizeValuesAndMapToIds(array $tags, int $pid): array { diff --git a/Classes/TcaHelper.php b/Classes/TcaHelper.php index 65da317..6276969 100644 --- a/Classes/TcaHelper.php +++ b/Classes/TcaHelper.php @@ -1,5 +1,7 @@ typo3Version = GeneralUtility::makeInstance(Typo3Version::class); + } + public function buildFieldConfiguration(string $table, string $fieldName, array $fieldConfigurationOverride = null): array { $fieldConfiguration = [ @@ -27,13 +39,17 @@ public function buildFieldConfiguration(string $table, string $fieldName, array 'items' => [], 'foreign_table' => 'sys_tag', 'MM' => 'sys_tag_mm', - 'MM_hasUidField' => true, 'MM_opposite_field' => 'items', 'MM_match_fields' => [ 'tablenames' => $table, 'fieldname' => $fieldName, ], ]; + + if ($this->typo3Version->getMajorVersion() === 12) { + $fieldConfiguration['MM_hasUidField'] = true; + } + // Merge changes to TCA configuration if (!empty($fieldConfigurationOverride)) { $fieldConfiguration = array_replace_recursive( @@ -55,9 +71,6 @@ public function buildFieldConfiguration(string $table, string $fieldName, array /** * Shorthand function to identify all fields that have tags based on the foreign_table field. - * - * @param string $table - * @return array */ public function findTagFieldsForTable(string $table): array { diff --git a/Configuration/Backend/AjaxRoutes.php b/Configuration/Backend/AjaxRoutes.php index 06b74f0..619256b 100644 --- a/Configuration/Backend/AjaxRoutes.php +++ b/Configuration/Backend/AjaxRoutes.php @@ -3,6 +3,6 @@ return [ 'tag_suggest_tags' => [ 'path' => '/tag/suggest', - 'target' => B13\Tag\Controller\SuggestReceiver::class . '::findSuitableTags' + 'target' => B13\Tag\Controller\SuggestReceiver::class . '::findSuitableTags', ], -]; \ No newline at end of file +]; diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml new file mode 100644 index 0000000..7fa2540 --- /dev/null +++ b/Configuration/Services.yaml @@ -0,0 +1,14 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + B13\Tag\: + resource: '../Classes/*' + + B13\Tag\Controller\SuggestReceiver: + public: true + + B13\Tag\Persistence\PrepareTagItems: + public: true diff --git a/Configuration/TCA/sys_tag.php b/Configuration/TCA/sys_tag.php index ae4ad3c..a70785c 100644 --- a/Configuration/TCA/sys_tag.php +++ b/Configuration/TCA/sys_tag.php @@ -1,20 +1,21 @@ [ 'title' => 'LLL:EXT:tag/Resources/Private/Language/locallang_tca.xlf:sys_tag', 'label' => 'name', 'tstamp' => 'updatedon', 'crdate' => 'createdon', - 'cruser_id' => 'createdby', 'delete' => 'deleted', 'default_sortby' => 'name', 'rootLevel' => -1, 'searchFields' => 'name', 'typeicon_classes' => [ - 'default' => 'mimetypes-x-sys_category' + 'default' => 'mimetypes-x-sys_category', ], 'security' => [ 'ignoreRootLevelRestriction' => true, + 'ignorePageTypeRestriction' => true, ], ], 'types' => [ @@ -30,14 +31,14 @@ 'config' => [ 'type' => 'input', 'width' => 200, - 'eval' => 'trim,required' - ] + 'eval' => 'trim', + 'required' => true, + ], ], 'items' => [ 'label' => 'LLL:EXT:tag/Resources/Private/Language/locallang_tca.xlf:sys_tag.items', 'config' => [ 'type' => 'group', - 'internal_type' => 'db', 'allowed' => '*', 'MM' => 'sys_tag_mm', 'MM_oppositeUsage' => [], diff --git a/Resources/Public/StyleSheets/tagsinput.css b/Resources/Public/StyleSheets/tagsinput.css index 61aed11..87ae8ec 100644 --- a/Resources/Public/StyleSheets/tagsinput.css +++ b/Resources/Public/StyleSheets/tagsinput.css @@ -37,7 +37,7 @@ } .bootstrap-tagsinput .tag { margin-right: 2px; - color: white; + color: #343434; } .bootstrap-tagsinput .tag [data-role="remove"] { margin-left: 8px; @@ -52,4 +52,15 @@ } .bootstrap-tagsinput .tag [data-role="remove"]:hover:active { box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); -} \ No newline at end of file +} + +@media (prefers-color-scheme: dark) { + .bootstrap-tagsinput { + background: #000; + color: #fff; + } + + .bootstrap-tagsinput .tag { + color: #fff; + } +} diff --git a/composer.json b/composer.json index 489ff25..d5bc55a 100644 --- a/composer.json +++ b/composer.json @@ -3,14 +3,10 @@ "type": "typo3-cms-extension", "description": "Manage tags in TYPO3 Core", "license": "GPL-2.0-or-later", - "config": { - "vendor-dir": "build/vendor" - }, "require": { "php": "^7.4 || ^8.0", - "doctrine/dbal": "^2.13 || ^3.0", - "typo3/cms-core": "^11.0 || ^12.0", - "typo3/cms-backend": "^11.0 || ^12.0" + "typo3/cms-core": "^12.0 || ^13.0", + "typo3/cms-backend": "^12.0 || ^13.0" }, "extra": { "typo3/cms": { @@ -21,5 +17,17 @@ "psr-4": { "B13\\Tag\\": "Classes/" } + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.68", + "phpstan/phpstan": "^1.12", + "saschaegerer/phpstan-typo3": "^1.10", + "typo3/coding-standards": "^0.8.0" + }, + "config": { + "allow-plugins": { + "typo3/cms-composer-installers": true, + "typo3/class-alias-loader": true + } } } diff --git a/ext_emconf.php b/ext_emconf.php deleted file mode 100755 index 6d03321..0000000 --- a/ext_emconf.php +++ /dev/null @@ -1,19 +0,0 @@ - 'Tags and Keywords', - 'description' => 'Builds a tag bundle package to allow tags to be handled and added.', - 'category' => 'be', - 'author' => 'b13 GmbH', - 'author_email' => 'typo3@b13.com', - 'state' => 'stable', - 'author_company' => '', - 'version' => '1.2.0', - 'constraints' => [ - 'depends' => [ - 'typo3' => '11.0.0-12.4.99', - ], - 'conflicts' => [], - 'suggests' => [], - ], -]; diff --git a/ext_localconf.php b/ext_localconf.php index 42ab933..205accf 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -3,7 +3,7 @@ $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1433089350] = [ 'nodeName' => 'tagList', - 'priority' => 60, + 'priority' => 40, 'class' => \B13\Tag\Form\TagListElement::class, ]; $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass']['b13/tag'] = \B13\Tag\Persistence\PrepareTagItems::class; diff --git a/ext_tables.sql b/ext_tables.sql index 8c88c36..23d7211 100755 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -4,7 +4,7 @@ CREATE TABLE sys_tag ( ); CREATE TABLE sys_tag_mm ( - uid int(11) NOT NULL auto_increment, + uid int(11) NOT NULL auto_increment, uid_local int(11) DEFAULT '0' NOT NULL, uid_foreign int(11) DEFAULT '0' NOT NULL, tablenames varchar(255) DEFAULT '' NOT NULL, @@ -12,7 +12,7 @@ CREATE TABLE sys_tag_mm ( sorting int(11) DEFAULT '0' NOT NULL, sorting_foreign int(11) DEFAULT '0' NOT NULL, - PRIMARY KEY (uid), + PRIMARY KEY (uid), KEY uid_local_foreign (uid_local,uid_foreign), KEY uid_foreign_tablefield (uid_foreign,tablenames(40),fieldname(3),sorting_foreign) ); diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..5ceba99 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,16 @@ +includes: + - vendor/phpstan/phpstan/conf/bleedingEdge.neon + - vendor/saschaegerer/phpstan-typo3/extension.neon + +parameters: + level: 5 + + paths: + - %currentWorkingDirectory%/ + + excludePaths: + analyseAndScan: + - */node_modules/* + - vendor/* + - public/* + - ./.php-cs-fixer.php