diff --git a/documentation/server/tools/typo3_fluid_namespace_list.rst b/documentation/server/tools/typo3_fluid_namespace_list.rst index 8e4647a4..a22cb2ed 100644 --- a/documentation/server/tools/typo3_fluid_namespace_list.rst +++ b/documentation/server/tools/typo3_fluid_namespace_list.rst @@ -6,9 +6,11 @@ List the Fluid ViewHelper namespaces that are globally available in the TYPO3 installation you are working in, so a template knows which prefixes it may use without declaring them. Every other namespace has to be declared per template -with an xmlns attribute. Where the console cannot be reached, the -Configuration/Fluid/Namespaces.php the installed packages declare is read -instead. Answers from: installation, packages. +with an xmlns attribute. On TYPO3 v14 and later, the fluid:namespaces console +command answers; where it cannot be reached, the +Configuration/Fluid/Namespaces.php files introduced in that version are read +instead. Earlier versions are booted and answered from SYS/fluid/namespaces in +TYPO3_CONF_VARS. Answers from: installation, packages. ``readOnlyHint: true`` · ``destructiveHint: false`` · ``idempotentHint: true`` · ``openWorldHint: false`` diff --git a/src/Installation/FluidNamespaces.php b/src/Installation/FluidNamespaces.php index 4888881e..4ec0bbe9 100644 --- a/src/Installation/FluidNamespaces.php +++ b/src/Installation/FluidNamespaces.php @@ -8,14 +8,15 @@ * The globally registered Fluid namespaces, read from the packages that declare * them. * - * `fluid:namespaces` is the better answer and stays the first one asked, because - * it reports what the container actually assembled. But it needs a console that + * On TYPO3 v14 and later, `fluid:namespaces` is the better answer because it + * reports what the container actually assembled. But it needs a console that * boots, and the declaration itself is a file: every package may ship a * Configuration/Fluid/Namespaces.php returning prefix => list of PHP namespaces, * and the core merges them across all active packages. * * The files are parsed, never included — the same rule as for the icon registry. - * They are ordinary PHP that would run in this process. + * They are ordinary PHP that would run in this process. Earlier TYPO3 versions + * register these namespaces in TYPO3_CONF_VARS and have no such file. */ final class FluidNamespaces { diff --git a/src/Tool/FluidNamespaceList.php b/src/Tool/FluidNamespaceList.php index 669f6c02..7a377243 100644 --- a/src/Tool/FluidNamespaceList.php +++ b/src/Tool/FluidNamespaceList.php @@ -5,7 +5,9 @@ namespace TYPO3\DevCompanion\Tool; use TYPO3\DevCompanion\Installation\FluidNamespaces; +use TYPO3\DevCompanion\Installation\Instance; use TYPO3\DevCompanion\Installation\Typo3Cli; +use TYPO3\DevCompanion\Installation\Typo3Runtime; use TYPO3\DevCompanion\Result\Schema; use TYPO3\DevCompanion\Result\ToolResult; use TYPO3\DevCompanion\Result\Unsupported; @@ -28,7 +30,7 @@ public static function answersFrom(): array public static function description(): string { - return 'List the Fluid ViewHelper namespaces that are globally available in the TYPO3 installation you are working in, so a template knows which prefixes it may use without declaring them. Every other namespace has to be declared per template with an xmlns attribute. Where the console cannot be reached, the Configuration/Fluid/Namespaces.php the installed packages declare is read instead.'; + return 'List the Fluid ViewHelper namespaces that are globally available in the TYPO3 installation you are working in, so a template knows which prefixes it may use without declaring them. Every other namespace has to be declared per template with an xmlns attribute. On TYPO3 v14 and later, the fluid:namespaces console command answers; where it cannot be reached, the Configuration/Fluid/Namespaces.php files introduced in that version are read instead. Earlier versions are booted and answered from SYS/fluid/namespaces in TYPO3_CONF_VARS.'; } public static function inputSchema(): array @@ -50,18 +52,38 @@ public static function outputSchema(): array public static function answer(array $args): ToolResult { - $answer = Typo3Cli::json(['fluid:namespaces', '--json']); $answeredBy = 'installation'; - $declared = is_array($answer['data']) ? $answer['data'] : []; - if (!$answer['ok'] || !is_array($answer['data'])) { - // The declarations are files in the same packages, so a console - // that cannot boot does not have to end the question. What the - // files cannot say is what the container did with them. - $declared = FluidNamespaces::all(); - if ($declared === []) { - return Unsupported::because($answer['error']); + $limitation = ''; + + if ((Instance::typo3Major() ?? 0) >= 14) { + $answer = Typo3Cli::json(['fluid:namespaces', '--json']); + $declared = is_array($answer['data']) ? $answer['data'] : []; + if (!$answer['ok'] || !is_array($answer['data'])) { + // The declarations are files in the same packages, so a + // console that cannot boot does not have to end the question. + // What the files cannot say is what the container did with + // them. + $declared = FluidNamespaces::all(); + if ($declared === []) { + return Unsupported::because($answer['error']); + } + $answeredBy = 'packages'; + $limitation = $answer['error']; + } + } else { + // Before v14 there is no fluid:namespaces command and no + // Configuration/Fluid/Namespaces.php. The assembled registry is + // the TYPO3_CONF_VARS value after every extension has run. + $read = Typo3Runtime::configuration('SYS/fluid/namespaces'); + if ($read === null || isset($read['unavailable'])) { + return Unsupported::because( + is_array($read) ? (string) $read['unavailable'] : Typo3Runtime::reason(), + ); + } + if ($read['found'] === true && !is_array($read['value'])) { + return Unsupported::because('SYS/fluid/namespaces is not an array in this installation'); } - $answeredBy = 'packages'; + $declared = $read['found'] === true ? $read['value'] : []; } $namespaces = []; @@ -81,12 +103,12 @@ public static function answer(array $args): ToolResult $lines[] = 'These prefixes work in any template without being declared. Every other namespace is declared ' . 'in the template itself — xmlns:be="http://typo3.org/ns/TYPO3/CMS/Backend/ViewHelpers" on the root ' . 'element, together with data-namespace-typo3-fluid="true" so the declaration is stripped from the output.'; - if ($answeredBy === 'packages') { + if ($limitation !== '') { $lines[] = ''; $lines[] = sprintf( 'Read from the Configuration/Fluid/Namespaces.php of the installed packages: the console could not ' . 'be asked (%s). That is what the packages declare, not what the container assembled from them.', - $answer['error'], + $limitation, ); } diff --git a/tests/Unit/FluidNamespaceListTest.php b/tests/Unit/FluidNamespaceListTest.php new file mode 100644 index 00000000..88b1b858 --- /dev/null +++ b/tests/Unit/FluidNamespaceListTest.php @@ -0,0 +1,123 @@ +coreCheckout($version); + Instance::discoverFrom($root); + putenv(Typo3Cli::CONSOLE_VARIABLE . '=' . PHP_BINARY . ' ' . $root . '/bin/typo3'); + Typo3Cli::forget(); + + $commands = []; + $runner = self::createStub(CommandRunner::class); + $runner->method('run')->willReturnCallback(static function (array $command) use (&$commands): array { + $commands[] = $command; + + return [ + 'ok' => true, + 'exitCode' => 0, + 'output' => json_encode([ + 'state' => Typo3Runtime::STATE_FULL, + 'reason' => '', + 'topics' => ['configuration' => [ + 'found' => true, + 'value' => ['acme' => ['Acme\\ViewHelpers']], + ]], + ], JSON_THROW_ON_ERROR), + 'error' => '', + ]; + }); + Typo3Cli::useRunner($runner); + + $result = Registry::call('typo3_fluid_namespace_list', []); + + self::assertSame('installation', $result->data['answeredBy']); + self::assertSame([[ + 'prefix' => 'acme', + 'phpNamespaces' => ['Acme\\ViewHelpers'], + ]], $result->data['namespaces']); + self::assertCount(1, $commands); + self::assertSame('-r', $commands[0][1]); + if (preg_match('/base64_decode\("([A-Za-z0-9+\/=]+)"\)/', $commands[0][2], $payload) !== 1) { + self::fail('The runtime probe was not passed as a base64-encoded PHP payload.'); + } + self::assertStringContainsString( + "'configurationPath' => 'SYS/fluid/namespaces'", + (string) base64_decode($payload[1], true), + ); + } + + /** @return iterable */ + public static function versionsBeforeFourteen(): iterable + { + yield 'TYPO3 v12' => ['12.4.42']; + yield 'TYPO3 v13' => ['13.4.22']; + } + + #[Test] + public function fourteenUsesTheFluidNamespaceCommand(): void + { + $root = $this->coreCheckout('14.0.0'); + Instance::discoverFrom($root); + putenv(Typo3Cli::CONSOLE_VARIABLE . '=' . PHP_BINARY . ' ' . $root . '/bin/typo3'); + Typo3Cli::forget(); + + $commands = []; + $runner = self::createStub(CommandRunner::class); + $runner->method('run')->willReturnCallback(static function (array $command) use (&$commands): array { + $commands[] = $command; + + return [ + 'ok' => true, + 'exitCode' => 0, + 'output' => '{"core":["TYPO3\\\\CMS\\\\Core\\\\ViewHelpers"]}', + 'error' => '', + ]; + }); + Typo3Cli::useRunner($runner); + + $result = Registry::call('typo3_fluid_namespace_list', []); + + self::assertSame('installation', $result->data['answeredBy']); + self::assertSame([ + PHP_BINARY, + $root . '/bin/typo3', + 'fluid:namespaces', + '--json', + '--no-interaction', + '--no-ansi', + ], $commands[0]); + self::assertSame('core', $result->data['namespaces'][0]['prefix']); + } +} diff --git a/tests/Unit/PackageSourcesTest.php b/tests/Unit/PackageSourcesTest.php index b8c8369b..e56c46cb 100644 --- a/tests/Unit/PackageSourcesTest.php +++ b/tests/Unit/PackageSourcesTest.php @@ -66,7 +66,7 @@ public function fluidNamespacesAreReadFromThePackagesThatDeclareThem(): void #[Test] public function withoutAConsoleTheDeclarationsAreTheAnswerAndSaySoAsOne(): void { - $root = $this->coreCheckout(); + $root = $this->coreCheckout('14.3.0'); $this->namespaceFile($root . '/typo3/sysext/core', ['core' => ['TYPO3\\CMS\\Core\\ViewHelpers']]); Instance::discoverFrom($root); Typo3Cli::forget();