diff --git a/README.md b/README.md index 798f7d5..22bc38d 100644 --- a/README.md +++ b/README.md @@ -314,6 +314,19 @@ To update the tags: ./vendor/bin/tailor ter:update my_extension --tags=some-tag,another-tag ``` +Tags are what people search the extension listing by, so they are worth +choosing for TER specifically. Every extension in TER is a TYPO3 extension, +which means terms such as `typo3`, `typo3-extension`, `extension`, `cms` or +`php` narrow nothing down there — even though the very same terms are what +make a package findable on GitHub or Packagist. Prefer tags describing what +the extension does. Tailor warns when it recognises such a term, but still +sends whatever you pass. + +This matters most for automated publishing: because the whole list is +replaced on every call (see below), a pipeline that reuses one vocabulary +across registries will keep overwriting curated TER tags with terms that add +nothing. + Please use `./vendor/bin/tailor ter:update -h` to see the full list of available options. diff --git a/src/Command/Extension/UpdateExtensionCommand.php b/src/Command/Extension/UpdateExtensionCommand.php index ab70fdf..5787076 100644 --- a/src/Command/Extension/UpdateExtensionCommand.php +++ b/src/Command/Extension/UpdateExtensionCommand.php @@ -16,6 +16,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; use TYPO3\Tailor\Command\AbstractClientRequestCommand; use TYPO3\Tailor\Dto\Messages; use TYPO3\Tailor\Dto\RequestConfiguration; @@ -51,15 +52,49 @@ protected function configure(): void ->addOption('repository', '', InputOption::VALUE_OPTIONAL, 'Link to the repository') ->addOption('manual', '', InputOption::VALUE_OPTIONAL, 'Link to the external manual') ->addOption('paypal', '', InputOption::VALUE_OPTIONAL, 'Link to sponsoring page (paypal)') - ->addOption('tags', '', InputOption::VALUE_OPTIONAL, 'Comma-separated list of tags'); + ->addOption( + 'tags', + '', + InputOption::VALUE_OPTIONAL, + 'Comma-separated list of tags. Replaces the existing list. Every extension in TER is a ' + . 'TYPO3 extension, so terms like typo3, typo3-extension, extension or php add no ' + . 'discoverability there - prefer tags describing what the extension does.' + ); } protected function execute(InputInterface $input, OutputInterface $output): int { $this->extensionKey = CommandHelper::getExtensionKeyFromInput($input); + $this->warnAboutTagsImpliedByTer($input, $output); + return parent::execute($input, $output); } + /** + * TER replaces the whole tag list with what is sent, so a pipeline reusing + * one vocabulary across registries silently publishes terms that say + * nothing here. Point them out without changing what gets sent. + */ + private function warnAboutTagsImpliedByTer(InputInterface $input, OutputInterface $output): void + { + $tags = $input->getOption('tags'); + if (!is_string($tags) || $tags === '') { + return; + } + + $implied = CommandHelper::getTagsImpliedByTer($tags); + if ($implied === []) { + return; + } + + (new SymfonyStyle($input, $output))->warning(sprintf( + 'Every extension in TER is a TYPO3 extension, so %s %s no discoverability there. ' + . 'Consider tags describing what the extension does instead.', + implode(', ', $implied), + count($implied) === 1 ? 'adds' : 'add' + )); + } + protected function getRequestConfiguration(): RequestConfiguration { return new RequestConfiguration( diff --git a/src/Helper/CommandHelper.php b/src/Helper/CommandHelper.php index d3a516b..f15a806 100644 --- a/src/Helper/CommandHelper.php +++ b/src/Helper/CommandHelper.php @@ -22,6 +22,31 @@ */ final class CommandHelper { + /** + * Terms every TER listing already implies. TER only lists TYPO3 extensions, + * so these narrow nothing down there — unlike on GitHub or Packagist, where + * the same terms are what make a package findable at all. + * + * Compared after stripping separators, so the spelling variants that occur + * in practice — "typo3 cms" in composer.json keywords, "typo3-cms-extension" + * in GitHub topics — are all recognised. + * + * @var string[] + */ + private const TAGS_IMPLIED_BY_TER = [ + 'cms', + 'cmsextension', + 'extension', + 'extensions', + 'php', + 'ter', + 'typo3', + 'typo3cms', + 'typo3cmsextension', + 'typo3ext', + 'typo3extension', + ]; + public static function getExtensionKeyFromInput(InputInterface $input): string { // 1. CLI argument has highest priority @@ -52,4 +77,29 @@ public static function getExtensionKeyFromInput(InputInterface $input): string 1605706548 ); } + + /** + * Returns those of the given tags that TER already implies, so the caller + * can point them out. Case and separators are ignored. + * + * @return string[] + */ + public static function getTagsImpliedByTer(string $tags): array + { + $implied = []; + + foreach (explode(',', $tags) as $tag) { + $tag = trim($tag); + if ($tag === '') { + continue; + } + + $normalized = strtolower((string)preg_replace('/[^a-z0-9]/i', '', $tag)); + if (in_array($normalized, self::TAGS_IMPLIED_BY_TER, true)) { + $implied[] = $tag; + } + } + + return $implied; + } } diff --git a/tests/Unit/Helper/CommandHelperTest.php b/tests/Unit/Helper/CommandHelperTest.php index c805a21..894d4aa 100644 --- a/tests/Unit/Helper/CommandHelperTest.php +++ b/tests/Unit/Helper/CommandHelperTest.php @@ -12,6 +12,7 @@ namespace TYPO3\Tailor\Tests\Unit\Helper; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Input\ArrayInput; @@ -98,4 +99,42 @@ public function getExtensionKeyFromInputReturnsExtensionKeyFromEnvironmentVariab restore_error_handler(); } } + + /** + * @param string[] $expected + */ + #[Test] + #[DataProvider('tagsImpliedByTerDataProvider')] + public function getTagsImpliedByTerReturnsOnlyTermsTerAlreadyImplies(string $tags, array $expected): void + { + self::assertSame($expected, CommandHelper::getTagsImpliedByTer($tags)); + } + + /** + * @return array + */ + public static function tagsImpliedByTerDataProvider(): array + { + return [ + 'empty input' => ['', []], + 'only domain tags' => ['search,indexing,facets', []], + 'single implied tag' => ['typo3,search', ['typo3']], + 'several implied tags' => ['typo3,php,search,extension', ['typo3', 'php', 'extension']], + 'case is ignored' => ['TYPO3,Extension', ['TYPO3', 'Extension']], + 'surrounding whitespace' => [' typo3 , search ', ['typo3']], + 'empty segments' => ['typo3,,search,', ['typo3']], + 'substring is not a match' => ['typo3-solr,phpunit', []], + // Spelling variants observed in the wild: composer.json keywords + // favour "typo3 cms", GitHub topics "typo3-cms-extension". + 'separator variants' => [ + 'typo3 cms,typo3-cms,typo3cms', + ['typo3 cms', 'typo3-cms', 'typo3cms'], + ], + 'extension variants' => [ + 'typo3-cms-extension,typo3-extension,cms-extension', + ['typo3-cms-extension', 'typo3-extension', 'cms-extension'], + ], + 'domain tag with separator survives' => ['e-commerce,tt_news', []], + ]; + } }