diff --git a/Classes/Command/CheckContentEscapingCommand.php b/Classes/Command/CheckContentEscapingCommand.php index f09d45a..aef3f3d 100644 --- a/Classes/Command/CheckContentEscapingCommand.php +++ b/Classes/Command/CheckContentEscapingCommand.php @@ -29,6 +29,7 @@ use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode; use TYPO3Fluid\Fluid\Core\Parser\TemplateParser; use TYPO3Fluid\Fluid\ViewHelpers\Format\RawViewHelper; +use function array_keys; class CheckContentEscapingCommand extends Command { @@ -69,14 +70,14 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { - $componentNamespaces = $this->componentLoader->getNamespaces(); + $componentNamespaces = array_keys($this->componentLoader->getNamespaces()); $templateFiles = $this->discoverTemplateFiles(); $progress = new ProgressBar($output, count($componentNamespaces) + count($templateFiles)); $progress->start(); // Determine which components use {content -> f:format.raw()} or similar - foreach ($componentNamespaces as $namespace => $path) { + foreach ($componentNamespaces as $namespace) { foreach ($this->componentLoader->findComponentsInNamespace($namespace) as $className => $file) { try { $template = $this->parseTemplate($file); diff --git a/Classes/Service/XsdGenerator.php b/Classes/Service/XsdGenerator.php index 5229ff2..a273ec0 100644 --- a/Classes/Service/XsdGenerator.php +++ b/Classes/Service/XsdGenerator.php @@ -7,6 +7,7 @@ use SMS\FluidComponents\Utility\ComponentLoader; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3Fluid\Fluid\Core\ViewHelper\ArgumentDefinition; +use function array_keys; class XsdGenerator { @@ -127,8 +128,8 @@ protected function getDefaultPrefixForNamespace(string $namespace): int|string public function generateXsd(string $path, ?string $namespace = null): array { $generatedNameSpaces = []; - $namespaces = $this->componentLoader->getNamespaces(); - foreach ($namespaces as $registeredNamespace => $registeredNamepacePath) { + $namespaces = array_keys($this->componentLoader->getNamespaces()); + foreach ($namespaces as $registeredNamespace) { if ($namespace === null || $registeredNamespace === $namespace) { $components = $this->componentLoader->findComponentsInNamespace($registeredNamespace); $filePath = rtrim((string) $path, DIRECTORY_SEPARATOR) . diff --git a/Classes/Utility/ComponentLoader.php b/Classes/Utility/ComponentLoader.php index 0021b05..de01b4b 100644 --- a/Classes/Utility/ComponentLoader.php +++ b/Classes/Utility/ComponentLoader.php @@ -6,6 +6,7 @@ class ComponentLoader implements \TYPO3\CMS\Core\SingletonInterface { /** * Registered component namespaces. + * @var array> */ protected array $namespaces = []; @@ -23,14 +24,15 @@ public function __construct() /** * Adds a new component namespace. + * @param string|list $path */ - public function addNamespace(string $namespace, string $path): self + public function addNamespace(string $namespace, string|array $path): self { // Sanitize namespace data $namespace = $this->sanitizeNamespace($namespace); - $path = $this->sanitizePath($path); + $paths = array_values(array_map($this->sanitizePath(...), (array)$path)); - $this->namespaces[$namespace] = $path; + $this->namespaces[$namespace] = $paths; return $this; } @@ -81,7 +83,7 @@ public function findComponent(string $class, string $ext = '.html'): string|null // Walk through available namespaces, ordered from specific to unspecific $class = ltrim($class, '\\'); - foreach ($this->namespaces as $namespace => $path) { + foreach ($this->namespaces as $namespace => $paths) { // No match, skip to next if (!str_starts_with($class, $namespace . '\\')) { continue; @@ -89,13 +91,16 @@ public function findComponent(string $class, string $ext = '.html'): string|null $componentParts = explode('\\', trim(substr($class, strlen($namespace)), '\\')); - $componentPath = $path . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $componentParts); - $componentFile = $componentPath . DIRECTORY_SEPARATOR . end($componentParts) . $ext; + foreach (array_reverse($paths) as $path) { + $componentPath = $path . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $componentParts); + $componentFile = $componentPath . DIRECTORY_SEPARATOR . end($componentParts) . $ext; - // Check if component file exists - if (file_exists($componentFile)) { - $this->componentsCache[$cacheIdentifier] = $componentFile; - return $componentFile; + // Check if component file exists + if (file_exists($componentFile)) { + $this->componentsCache[$cacheIdentifier] = $componentFile; + // first path found is the most specific one, so we can return it immediately + return $componentFile; + } } } @@ -110,17 +115,23 @@ public function findComponent(string $class, string $ext = '.html'): string|null */ public function findComponentsInNamespace(string $namespace, string $ext = '.html'): array { - if (!isset($this->namespaces[$namespace]) || !is_dir($this->namespaces[$namespace])) { + $paths = $this->namespaces[$namespace] ?? null; + if (!$paths) { return []; } - $scannedPaths = []; - return $this->scanForComponents( - $this->namespaces[$namespace], - $ext, - $namespace, - $scannedPaths - ); + $components = []; + foreach ($paths as $path) { + if (!is_dir($path)) { + continue; + } + + $scannedPaths = []; + // overwrite previous locations with newer locations + $components = [...$components, ...$this->scanForComponents($path, $ext, $namespace, $scannedPaths)]; + } + + return $components; } /** diff --git a/README.md b/README.md index 75c9a97..24abc5b 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ or [via composer](https://packagist.org/packages/sitegeist/fluid-components): 2. Define the component namespace in your *ext_localconf.php*: ```php - $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['fluid_components']['namespaces']['VENDOR\\MyExtension\\Components'] = + $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['fluid_components']['namespaces']['VENDOR\\MyExtension\\Components'][] = \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('my_extension', 'Resources/Private/Components'); ``` diff --git a/Tests/Functional/ComponentRendererTest.php b/Tests/Functional/ComponentRendererTest.php index 350370a..b939f0f 100644 --- a/Tests/Functional/ComponentRendererTest.php +++ b/Tests/Functional/ComponentRendererTest.php @@ -40,7 +40,7 @@ public function setUp(): void // Register test components $this->componentNamespaces = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['fluid_components']['namespaces'] ?? null; $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['fluid_components']['namespaces'] = [ - $this->testNamespace => realpath(__DIR__ . '/../Fixtures/Functional/Components/'), + $this->testNamespace => [realpath(__DIR__ . '/../Fixtures/Functional/Components/')], ]; // Register and then disable fluid cache diff --git a/Tests/Unit/Domain/Model/LabelsTest.php b/Tests/Unit/Domain/Model/LabelsTest.php index 8a69d75..7ac62b7 100644 --- a/Tests/Unit/Domain/Model/LabelsTest.php +++ b/Tests/Unit/Domain/Model/LabelsTest.php @@ -12,7 +12,7 @@ protected function setUp(): void parent::setUp(); $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['fluid_components']['namespaces'] = [ - 'SMS\FluidComponents\Tests' => realpath(__DIR__ . '/../../../Fixtures/Unit/ComponentLoader'), + 'SMS\FluidComponents\Tests' => [realpath(__DIR__ . '/../../../Fixtures/Unit/ComponentLoader')], ]; } diff --git a/Tests/Unit/Utility/ComponentLoaderTest.php b/Tests/Unit/Utility/ComponentLoaderTest.php index db7d462..6ec28b0 100644 --- a/Tests/Unit/Utility/ComponentLoaderTest.php +++ b/Tests/Unit/Utility/ComponentLoaderTest.php @@ -30,14 +30,27 @@ public static function addNamespaceProvider() '\\Vendor\\Extension\\Category\\', '/path/to/components', [ - 'Vendor\\Extension\\Category' => '/path/to/components', + 'Vendor\\Extension\\Category' => ['/path/to/components'], ], ], 'pathWithTrailingSlash' => [ 'Vendor\\Extension\\Category\\', '/path/to/components/', [ - 'Vendor\\Extension\\Category' => '/path/to/components', + 'Vendor\\Extension\\Category' => ['/path/to/components'], + ], + ], + 'pathsWithTrailingSlash' => [ + 'Vendor\\Extension\\Category\\', + [ + '/path/to/components', + '/path/to/components_overwrites', + ], + [ + 'Vendor\\Extension\\Category' => [ + '/path/to/components', + '/path/to/components_overwrites', + ], ], ], ]; @@ -45,7 +58,7 @@ public static function addNamespaceProvider() #[Test] #[DataProvider('addNamespaceProvider')] - public function addNamespace(string $namespace, string $path, array $namespaces): void + public function addNamespace(string $namespace, string|array $path, array $namespaces): void { $this->loader->addNamespace($namespace, $path); $this->assertEquals( @@ -74,22 +87,22 @@ public static function setNamespacesProvider() return [ 'case1' => [ [ - 'Sitegeist\\Fixtures\\ComponentLoader' => '/case1/path1', - 'Sitegeist\\Fixtures\\Component\\Loader' => '/case1/path2', - 'Sitegeist\\Fixtures\\ComponentLoader\\Test' => '/case1/path3', - 'Vendor\\Test\\Namespace' => '/case1/path4', - '\\Sitegeist\\Fixtures\\ComponentLoader' => '/case1/path5', - '\\Sitegeist\\Fixtures\\ComponentLoader\\' => '/case1/path6', - '\\Sitegeist\\Fixtures\\AnotherTest\\' => '/case1/path7', - '\\Sitegeist\\Fixtures\\Test\\' => '/case1/path8', + 'Sitegeist\\Fixtures\\ComponentLoader' => ['/case1/path1'], + 'Sitegeist\\Fixtures\\Component\\Loader' => ['/case1/path2'], + 'Sitegeist\\Fixtures\\ComponentLoader\\Test' => ['/case1/path3'], + 'Vendor\\Test\\Namespace' => ['/case1/path4'], + '\\Sitegeist\\Fixtures\\ComponentLoader' => ['/case1/path5'], + '\\Sitegeist\\Fixtures\\ComponentLoader\\' => ['/case1/path6'], + '\\Sitegeist\\Fixtures\\AnotherTest\\' => ['/case1/path7'], + '\\Sitegeist\\Fixtures\\Test\\' => ['/case1/path8'], ], [ - 'Vendor\\Test\\Namespace' => '/case1/path4', - 'Sitegeist\\Fixtures\\Test' => '/case1/path8', - 'Sitegeist\\Fixtures\\AnotherTest' => '/case1/path7', - 'Sitegeist\\Fixtures\\ComponentLoader\\Test' => '/case1/path3', - 'Sitegeist\\Fixtures\\ComponentLoader' => '/case1/path6', - 'Sitegeist\\Fixtures\\Component\\Loader' => '/case1/path2', + 'Vendor\\Test\\Namespace' => ['/case1/path4'], + 'Sitegeist\\Fixtures\\Test' => ['/case1/path8'], + 'Sitegeist\\Fixtures\\AnotherTest' => ['/case1/path7'], + 'Sitegeist\\Fixtures\\ComponentLoader\\Test' => ['/case1/path3'], + 'Sitegeist\\Fixtures\\ComponentLoader' => ['/case1/path6'], + 'Sitegeist\\Fixtures\\Component\\Loader' => ['/case1/path2'], ], ], ];