From 4cceed999d5d2f9e3bf5d04a885a493e4f6bbbc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Nagy?= Date: Tue, 18 Aug 2026 15:02:20 +0200 Subject: [PATCH 1/9] Overwrite 'i18n extract' to make it able to parse twig templates too. --- src/Command/TwigExtractCommand.php | 359 ++++++++++++++++++ src/TwigViewPlugin.php | 2 + .../Command/I18nExtractCommandTest.php | 244 ++++++++++++ tests/test_app/templates/i18n/i18n_test.twig | 79 ++++ 4 files changed, 684 insertions(+) create mode 100644 src/Command/TwigExtractCommand.php create mode 100644 tests/TestCase/Command/I18nExtractCommandTest.php create mode 100644 tests/test_app/templates/i18n/i18n_test.twig diff --git a/src/Command/TwigExtractCommand.php b/src/Command/TwigExtractCommand.php new file mode 100644 index 0000000..81443b2 --- /dev/null +++ b/src/Command/TwigExtractCommand.php @@ -0,0 +1,359 @@ +helper('progress'); + assert($progress instanceof ProgressHelper); + $progress->init(['total' => count($this->_files)]); + $isVerbose = $args->getOption('verbose'); + + $functions = [ + '__' => ['singular'], + '__n' => ['singular', 'plural'], + '__d' => ['domain', 'singular'], + '__dn' => ['domain', 'singular', 'plural'], + '__x' => ['context', 'singular'], + '__xn' => ['context', 'singular', 'plural'], + '__dx' => ['domain', 'context', 'singular'], + '__dxn' => ['domain', 'context', 'singular', 'plural'], + ]; + $pattern = '/(' . implode('|', array_keys($functions)) . ')\s*\(/'; + + foreach ($this->_files as $file) { + $this->_file = $file; + if ($isVerbose) { + $io->verbose(sprintf('Processing %s...', $file)); + } + if (pathinfo($file, PATHINFO_EXTENSION) === 'twig') { + if ($this->_isTwigUsable()) { + $_parser = 'twig'; + } else { + $io->warning('Twig is not installed. Please install Twig to extract translations from twig templates.'); + continue; + } + $_parser = 'twig'; + } else { + $_parser = 'php'; + } + $code = (string)file_get_contents($file); + + if (preg_match($pattern, $code) === 1) { + if ($_parser === 'twig') { + $this->_tokenizeAsTwig($code, $file); + } else { + $this->_tokenizeAsPHP($code, $file); + } + + foreach ($functions as $functionName => $map) { + if ($_parser === 'twig') { + $this->_parseAsTwig($io, $functionName, $map); + } else { + $this->_parseAsPHP($io, $functionName, $map); + } + } + } + + if (!$isVerbose) { + $progress->increment(1); + $progress->draw(); + } + } + } + + /** + * Parse tokens + * + * @param \Cake\Console\ConsoleIo $io The io instance + * @param string $functionName Function name that indicates translatable string (e.g: '__') + * @param array $map Array containing what variables it will find (e.g: domain, singular, plural) + * @return void + */ + protected function _parseAsPHP(ConsoleIo $io, string $functionName, array $map): void + { + $count = 0; + $tokenCount = \count($this->_tokens); + + while ($tokenCount - $count > 1) { + $countToken = $this->_tokens[$count]; + $firstParenthesis = $this->_tokens[$count + 1]; + if (!is_array($countToken)) { + $count++; + continue; + } + + [$type, $string, $line] = $countToken; + if (($type === T_STRING) && ($string === $functionName) && ($firstParenthesis === '(')) { + $position = $count; + $depth = 0; + + while (!$depth) { + if ($this->_tokens[$position] === '(') { + $depth++; + } elseif ($this->_tokens[$position] === ')') { + $depth--; + } + $position++; + } + + $mapCount = count($map); + $strings = $this->_getStrings($position, $mapCount); + + if ($mapCount === count($strings)) { + $singular = ''; + $vars = array_combine($map, $strings); + extract($vars); + $domain ??= 'default'; + $details = [ + 'file' => $this->_file, + 'line' => $line, + ]; + $details['file'] = '.' . str_replace(ROOT, '', $details['file']); + if (isset($plural)) { + $details['msgid_plural'] = $plural; + } + if (isset($context)) { + $details['msgctxt'] = $context; + } + $this->_addTranslation($domain, $singular, $details); + } else { + $this->_markerError($io, $this->_file, $line, $functionName, $count); + } + } + $count++; + } + } + + /** + * Parse Twig tokens + * + * @param \Cake\Console\ConsoleIo $io The io instance + * @param string $functionName Function name that indicates translatable string (e.g: '__') + * @param array $map Array containing what variables it will find (e.g: domain, singular, plural) + * @return void + */ + protected function _parseAsTwig(ConsoleIo $io, string $functionName, array $map): void + { + /** @var \Twig\Token $token */ + foreach ($this->_tokens as $count => $token) { + if ($token->test(Token::NAME_TYPE, $functionName)) { + switch ($functionName) { + case '__': + $singular = $this->_getStringFromToken($count, 2); + break; + case '__n': + $singular = $this->_getStringFromToken($count, 2); + $plural = $this->_getStringFromToken($count, 4); + break; + case '__d': + $domain = $this->_getStringFromToken($count, 2); + $singular = $this->_getStringFromToken($count, 4); + break; + case '__dn': + $domain = $this->_getStringFromToken($count, 2); + $singular = $this->_getStringFromToken($count, 4); + $plural = $this->_getStringFromToken($count, 6); + break; + case '__x': + $context = $this->_getStringFromToken($count, 2); + $singular = $this->_getStringFromToken($count, 4); + break; + case '__xn': + $context = $this->_getStringFromToken($count, 2); + $singular = $this->_getStringFromToken($count, 4); + $plural = $this->_getStringFromToken($count, 6); + break; + case '__dx': + $domain = $this->_getStringFromToken($count, 2); + $context = $this->_getStringFromToken($count, 4); + $singular = $this->_getStringFromToken($count, 6); + break; + case '__dxn': + $domain = $this->_getStringFromToken($count, 2); + $context = $this->_getStringFromToken($count, 4); + $singular = $this->_getStringFromToken($count, 6); + $plural = $this->_getStringFromToken($count, 8); + break; + } + $domain ??= 'default'; + $details = [ + 'file' => $this->_file, + 'line' => $token->getLine(), + ]; + $details['file'] = '.' . str_replace(ROOT, '', $details['file']); + if (in_array('plural', $map)) { + if (isset($plural)) { + $details['msgid_plural'] = $plural; + } else { + $this->_markerError($io, $this->_file, $token->getLine(), $functionName, $token->getOffset()); + continue; + } + } + + if (in_array('context', $map)) { + if (isset($context)) { + $details['msgctxt'] = $context; + } else { + $this->_markerError($io, $this->_file, $token->getLine(), $functionName, $token->getOffset()); + continue; + } + } + $this->_addTranslation($domain, $singular, $details); + } + } + } + + /** + * Search files that may contain translatable strings + * + * @return void + */ + protected function _searchFiles(): void + { + $pattern = false; + if ($this->_exclude) { + $exclude = []; + foreach ($this->_exclude as $e) { + if (DIRECTORY_SEPARATOR !== '\\' && !str_starts_with($e, DIRECTORY_SEPARATOR)) { + $e = DIRECTORY_SEPARATOR . $e; + } + $exclude[] = preg_quote($e, '/'); + } + $pattern = '/' . implode('|', $exclude) . '/'; + } + + foreach ($this->_paths as $path) { + $path = realpath($path); + if ($path === false) { + continue; + } + $path .= DIRECTORY_SEPARATOR; + $fs = new Filesystem(); + $files = $fs->findRecursive($path, '/\.php$|\.twig$/'); + $files = array_keys(iterator_to_array($files)); + sort($files); + if ($pattern) { + $files = preg_grep($pattern, $files, PREG_GREP_INVERT) ?: []; + $files = array_values($files); + } + $this->_files = array_merge($this->_files, $files); + } + $this->_files = array_unique($this->_files); + } + + /** + * Checks whether the Twig templating system is available. + * + * @return bool true if Twig is autoloadable and usable, false otherwise + */ + protected function _isTwigUsable(): bool + { + if (!class_exists('Twig\Environment')) { + return false; + } + + return true; + } + + /** + * Parses the given PHP source code for tokens, filtering out whitespace and inline HTML. + * + * @param string $code Source code of the file to parse + * @param string $file File name and path of the file to parse + * @return void + */ + protected function _tokenizeAsPHP(string $code, string $file): void + { + $allTokens = token_get_all($code); + $this->_tokens = []; + foreach ($allTokens as $token) { + if (!is_array($token) || ($token[0] !== T_WHITESPACE && $token[0] !== T_INLINE_HTML)) { + $this->_tokens[] = $token; + } + } + unset($allTokens); + } + + /** + * Parses the given Twig source code for tokens. + * + * @param string $code Source code of the file to parse + * @param string $file File name and path of the file to parse + * @return void + */ + protected function _tokenizeAsTwig(string $code, string $file): void + { + $twig = new \Twig\Environment(new \Twig\Loader\ArrayLoader()); + /** + * @var \Twig\TokenStream $stream + */ + $stream = $twig->tokenize(new \Twig\Source(code: $code, name: $file, path: $file)); + $this->_tokens = []; + while (!$stream->isEOF()) { + $token = $stream->next(); + if (! $token->test(\Twig\Token::TEXT_TYPE) && ! $token->test(\Twig\Token::BLOCK_END_TYPE)) { + $this->_tokens[] = $token; + } + } + unset($stream); + } + + protected function _getStringFromToken(int $position, int $offset):string { + $string = $this->_tokens[$position + $offset]->getValue(); + return str_replace('"', '\"', $string); + } +} diff --git a/src/TwigViewPlugin.php b/src/TwigViewPlugin.php index 1037f32..e846494 100644 --- a/src/TwigViewPlugin.php +++ b/src/TwigViewPlugin.php @@ -22,6 +22,7 @@ use Cake\Core\BasePlugin; use Cake\Core\Configure; use Cake\TwigView\Command\CompileCommand; +use Cake\TwigView\Command\TwigExtractCommand; /** * Plugin class for Cake\TwigView. @@ -49,6 +50,7 @@ public function console(CommandCollection $commands): CommandCollection // Deprecated: use `'TwigView.useUnderscoreCommands' => true` to switch to `twig_view compile` $commands->add('twig-view compile', CompileCommand::class); } + $commands->add('i18n extract', TwigExtractCommand::class); return $commands; } diff --git a/tests/TestCase/Command/I18nExtractCommandTest.php b/tests/TestCase/Command/I18nExtractCommandTest.php new file mode 100644 index 0000000..2ae3978 --- /dev/null +++ b/tests/TestCase/Command/I18nExtractCommandTest.php @@ -0,0 +1,244 @@ +path = TMP . 'tests/extract_task_test'; + $fs = new Filesystem(); + $fs->deleteDir($this->path); + $fs->mkdir($this->path . DS . 'locale'); + + Router::reload(); + Configure::write('App.encoding', 'UTF-8'); + + $this->loadPlugins(['Cake/TwigView']); + $this->setAppNamespace(); + } + + /** + * tearDown method + */ + protected function tearDown(): void + { + parent::tearDown(); + + $fs = new Filesystem(); + $fs->deleteDir($this->path); + $this->clearPlugins(); + } + + /** + * testExecute method + */ + public function testExecute(): void + { + $this->exec( + 'i18n extract ' . + '--merge=no ' . + '--extract-core=no ' . + '--paths=' . TEST_APP . 'templates' . DS . 'i18n ' . DS. + '--output=' . $this->path . DS, + [ + $this->path, + ], + ); + $this->assertExitSuccess(); + $this->assertFileExists($this->path . DS . 'default.pot'); + $this->assertFileExists($this->path . DS . 'test.pot'); + $this->assertFileDoesNotExist($this->path . DS . 'cake.pot'); + + $result = file_get_contents($this->path . DS . 'default.pot'); + + // The additional "./tests/test_app" is just due to the wonky folder structure of the test app. + // In a regular app the path would start with "./templates". + + // no_domain + $pattern = '@(\#: \./tests/test_app/templates/i18n/i18n_test\.twig:\d+\n)+'; + $pattern .= 'msgid "no_domain"@'; + $this->assertMatchesRegularExpression($pattern, $result); + + // no_domain, var + $pattern = '@(\#: \./tests/test_app/templates/i18n/i18n_test\.twig:\d+\n)+'; + $pattern .= 'msgid "no_domain_with_\{var\}"@'; + $this->assertMatchesRegularExpression($pattern, $result); + + // no_domain, var, context + $pattern = '@(\#: \./tests/test_app/templates/i18n/i18n_test\.twig:\d+\n)+'; + $pattern .= 'msgctxt "Context"\n'; + $pattern .= 'msgid "no_domain_with_context_with_\{var\}"@'; + $this->assertMatchesRegularExpression($pattern, $result); + + // no_domain, plural + $pattern = '@(\#: \./tests/test_app/templates/i18n/i18n_test\.twig:\d+\n)+'; + $pattern .= 'msgid "no_domain_singular"\nmsgid_plural "no_domain_plural"@'; + $this->assertMatchesRegularExpression($pattern, $result); + + // no_domain, plural, context + $pattern = '@(\#: \./tests/test_app/templates/i18n/i18n_test\.twig:\d+\n)+'; + $pattern .= 'msgctxt "Context"\n'; + $pattern .= 'msgid "no_domain_singular_with_context"\nmsgid_plural "no_domain_plural_with_context"@'; + $this->assertMatchesRegularExpression($pattern, $result); + + // no_domain, plural, var + $pattern = '@(\#: \./tests/test_app/templates/i18n/i18n_test\.twig:\d+\n)+'; + $pattern .= 'msgid "no_domain_singular_with_\{var\}"\nmsgid_plural "no_domain_plural_with_\{var\}"@'; + $this->assertMatchesRegularExpression($pattern, $result, 'No duplicate msgid'); + + // no_domain, plural, var, context + $pattern = '@(\#: \./tests/test_app/templates/i18n/i18n_test\.twig:\d+\n)+'; + $pattern .= 'msgctxt "Context"\n'; + $pattern .= 'msgid "no_domain_singular_with_context_with_\{var\}"\nmsgid_plural "no_domain_plural_with_context_with_\{var\}"@'; + $this->assertMatchesRegularExpression($pattern, $result, 'No duplicate msgid'); + + + $this->assertStringContainsString('msgid "double \\"quoted\\""', $result, 'Strings with quotes not handled correctly'); + $this->assertStringContainsString("msgid \"single 'quoted'\"", $result, 'Strings with quotes not handled correctly'); + + // test.pot + $result = file_get_contents($this->path . DS . 'test.pot'); + + // test_domain + $pattern = '@(\#: \./tests/test_app/templates/i18n/i18n_test\.twig:\d+\n)+'; + $pattern .= 'msgid "test_domain"@'; + $this->assertMatchesRegularExpression($pattern, $result); + + // test_domain, var + $pattern = '@(\#: \./tests/test_app/templates/i18n/i18n_test\.twig:\d+\n)+'; + $pattern .= 'msgid "test_domain_with_{var}"@'; + $this->assertMatchesRegularExpression($pattern, $result); + + // test_domain, var, context + $pattern = '@(\#: \./tests/test_app/templates/i18n/i18n_test\.twig:\d+\n)+'; + $pattern .= 'msgctxt "Context"\n'; + $pattern .= 'msgid "test_domain_with_context_with_\{var\}"@'; + $this->assertMatchesRegularExpression($pattern, $result); + + // test_domain, plural + $pattern = '@(\#: \./tests/test_app/templates/i18n/i18n_test\.twig:\d+\n)+'; + $pattern .= 'msgid "test_domain_singular"\nmsgid_plural "test_domain_plural"@'; + $this->assertMatchesRegularExpression($pattern, $result); + + // test_domain, plural, context + $pattern = '@(\#: \./tests/test_app/templates/i18n/i18n_test\.twig:\d+\n)+'; + $pattern .= 'msgctxt "Context"\n'; + $pattern .= 'msgid "test_domain_singular_with_context"\nmsgid_plural "test_domain_plural_with_context"@'; + $this->assertMatchesRegularExpression($pattern, $result); + + // test_domain, plural, var + $pattern = '@(\#: \./tests/test_app/templates/i18n/i18n_test\.twig:\d+\n)+'; + $pattern .= 'msgid "test_domain_singular_with_\{var\}"\nmsgid_plural "test_domain_plural_with_\{var\}"@'; + $this->assertMatchesRegularExpression($pattern, $result, 'No duplicate msgid'); + + // test_domain, plural, var, context + $pattern = '@(\#: \./tests/test_app/templates/i18n/i18n_test\.twig:\d+\n)+'; + $pattern .= 'msgctxt "Context"\n'; + $pattern .= 'msgid "test_domain_singular_with_context_with_\{var\}"\nmsgid_plural "test_domain_plural_with_context_with_\{var\}"@'; + $this->assertMatchesRegularExpression($pattern, $result, 'No duplicate msgid'); + + $this->assertStringContainsString('msgid "double \\"quoted\\""', $result, 'Strings with quotes not handled correctly'); + $this->assertStringContainsString("msgid \"single 'quoted'\"", $result, 'Strings with quotes not handled correctly'); + } + + /** + * testExecute with no paths + */ + public function testExecuteNoOutputOption(): void + { + $this->exec( + 'i18n extract ' . + '--merge=no ' . + '--extract-core=no ' . + '--paths=' . TEST_APP . 'templates' . DS . 'i18n ' , + [ + $this->path, + TEST_APP . 'templates' . DS . 'i18n' . DS, + 'D', + ], + ); + $this->assertExitSuccess(); + $this->assertFileExists($this->path . DS . 'default.pot'); + } + + /** + * testExecute with merging on method + */ + public function testExecuteMerge(): void + { + $this->exec( + 'i18n extract ' . + '--merge=yes ' . + '--extract-core=no ' . + '--paths=' . TEST_APP . 'templates' . DS . 'i18n ' . + '--output=' . $this->path . DS, + [ + $this->path, + ] + ); + $this->assertExitSuccess(); + $this->assertFileExists($this->path . DS . 'default.pot'); + $this->assertFileDoesNotExist($this->path . DS . 'cake.pot'); + $this->assertFileDoesNotExist($this->path . DS . 'domain.pot'); + } + + /** + * test exclusions + */ + public function testExtractWithExclude(): void + { + $this->exec( + 'i18n extract ' . + '--extract-core=no ' . + '--exclude=Pages,Layout ' . + '--paths=' . TEST_APP . 'templates' . DS . ' ' . + '--output=' . $this->path . DS, + ); + $this->assertExitSuccess(); + $this->assertFileExists($this->path . DS . 'default.pot'); + $result = file_get_contents($this->path . DS . 'default.pot'); + + $pattern = '/\#: .*extract\.php:\d+\n/'; + $this->assertDoesNotMatchRegularExpression($pattern, $result); + + $pattern = '/\#: .*default\.php:\d+\n/'; + $this->assertDoesNotMatchRegularExpression($pattern, $result); + } +} diff --git a/tests/test_app/templates/i18n/i18n_test.twig b/tests/test_app/templates/i18n/i18n_test.twig new file mode 100644 index 0000000..6c8d8ab --- /dev/null +++ b/tests/test_app/templates/i18n/i18n_test.twig @@ -0,0 +1,79 @@ +{{ __('no_domain') }} +{{ __('no_domain_with_{var}', {var: 'var'}) }} +{{ __n('no_domain_singular', 'no_domain_plural', 2) }} +{{ __n('no_domain_singular_with_{var}', 'no_domain_plural_with_{var}', 2, {var: 'var'}) }} +{{ __x('Context', 'no_domain_with_context') }} +{{ __x('Context', 'no_domain_with_context_with_{var}') }} +{{ __xn('Context','no_domain_singular_with_context', 'no_domain_plural_with_context', 2) }} +{{ __xn('Context','no_domain_singular_with_context_with_{var}', 'no_domain_plural_with_context_with_{var}', 2, {var: 'var'}) }} +{# test_domain #} +{{ __d('test', 'test_domain') }} +{{ __d('test', 'test_domain_with_{var}', {var: 'var'}) }} +{{ __dn('test', 'test_domain_singular', 'test_domain_plural', 2) }} +{{ __dn('test', + 'test_domain_singular_with_{var}', + 'test_domain_plural_with_{var}', 2, {var: 'var'}, +) }} +{{ __dx('test', 'Context', 'test_domain_with_context') }} +{{ __dx('test', 'Context', 'test_domain_with_context_with_{var}', {var: 'var'}) }} +{{ __dxn('test', 'Context', + 'test_domain_singular_with_context', + 'test_domain_plural_with_context', 2) }} +{{ __dxn('test', 'Context', + 'test_domain_singular_with_context_with_{var}', + 'test_domain_plural_with_context_with_{var}', + 2, + {var: 'var'}, +) }} + +{# multiline #} +{{ __('no_domain' ~ + '_multiline') }} +{{ __('no_domain' ~ + '_multiline_with_{var}', {var: 'var'}) }} +{{ __n('no_domain' ~ + '_multiline_singular', 'no_domain' ~ + '_multiline_plural', 2) }} +{{ __n('no_domain' ~ + '_multiline_singular_with_{var}', 'no_domain' ~ + '_multiline_plural_with_{var}', 2, {var: 'var'}) }} +{{ __x('Context', 'no_domain' ~ + '_multiline_with_context') }} +{{ __x('Context', 'no_domain' ~ + '_multiline_with_context_with_{var}') }} +{{ __xn('Context', 'no_domain' ~ + '_multiline_singular', 'no_domain' ~ + '_multiline_plural', 2) }} +{{ __xn('Context', 'no_domain' ~ + '_multiline_singular_with_{var}', 'no_domain' ~ + '_multiline_plural_with_{var}', 2, {var: 'var'}) }} +{# multiline test_domain #} +{{ __d('test', 'test_domain' ~ + '_multiline') }} +{{ __d('test', 'test_domain' ~ + '_multiline_with_{var}', {var: 'var'}) }} +{{ __dn('test', 'test_domain' ~ + '_multiline_singular', 'test_domain' ~ + '_multiline_plural', 2) }} +{{ __dn('test', + 'test_domain' ~ + '_multiline _singular_with_{var}', + 'test_domain' ~ + '_multiline_plural_with_{var}', 2, {var: 'var'}, +) }} +{{ __dx('test', 'Context', 'test_domain' ~ + '_multiline_with_context') }} +{{ __dx('test', 'Context', 'test_domain' ~ + '_multiline_with_context_with_context_with_{var}', {var: 'var'}) }} +{{ __dxn('test', 'Context', 'test_domain' ~ + '_multiline_with_context_singular', 'test_domain' ~ + '_multiline_with_context_plural', 2) }} +{{ __dxn('test', 'Context', 'test_domain' ~ + '_multiline_singular_with_context_with_{var}', 'test_domain' ~ + '_multiline_plural_with_context_with_{var}', 2, {var: 'var'}) }} +{# Contains quotes #} +{{ __('double "quoted"')}} +{{ __("single 'quoted'")}} +{# Contains quotes #} +{{ __d('test', 'double "quoted"')}} +{{ __d('test', "single 'quoted'")}} From 383a348f0cc1ea51ebc6b0bec3d9319692fc2f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Nagy?= Date: Tue, 18 Aug 2026 15:18:16 +0200 Subject: [PATCH 2/9] Fix PHP code style issues based on rector --- src/Command/TwigExtractCommand.php | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Command/TwigExtractCommand.php b/src/Command/TwigExtractCommand.php index 81443b2..77e46cc 100644 --- a/src/Command/TwigExtractCommand.php +++ b/src/Command/TwigExtractCommand.php @@ -302,11 +302,7 @@ protected function _searchFiles(): void */ protected function _isTwigUsable(): bool { - if (!class_exists('Twig\Environment')) { - return false; - } - - return true; + return class_exists(\Twig\Environment::class); } /** @@ -338,9 +334,6 @@ protected function _tokenizeAsPHP(string $code, string $file): void protected function _tokenizeAsTwig(string $code, string $file): void { $twig = new \Twig\Environment(new \Twig\Loader\ArrayLoader()); - /** - * @var \Twig\TokenStream $stream - */ $stream = $twig->tokenize(new \Twig\Source(code: $code, name: $file, path: $file)); $this->_tokens = []; while (!$stream->isEOF()) { From 858dd7f930761f2bea4f390dcaa65856cc71c8d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Nagy?= Date: Tue, 18 Aug 2026 15:30:09 +0200 Subject: [PATCH 3/9] Additional style fixes based on phpcs --- src/Command/TwigExtractCommand.php | 30 ++++++++++++++----- .../Command/I18nExtractCommandTest.php | 8 ++--- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/Command/TwigExtractCommand.php b/src/Command/TwigExtractCommand.php index 77e46cc..da63e2f 100644 --- a/src/Command/TwigExtractCommand.php +++ b/src/Command/TwigExtractCommand.php @@ -17,15 +17,18 @@ namespace Cake\TwigView\Command; -use Cake\Command\I18nExtractCommand; use Cake\Command\Helper\ProgressHelper; +use Cake\Command\I18nExtractCommand; use Cake\Console\Arguments; use Cake\Console\ConsoleIo; use Cake\Utility\Filesystem; +use Twig\Environment; +use Twig\Loader\ArrayLoader; +use Twig\Source; use Twig\Token; use function count; // Imports the global function -use function is_array; // Imports the global function use function in_array; // Imports the global function +use function is_array; // Imports the global function /** * Language string extractor @@ -126,7 +129,7 @@ protected function _extractTokens(Arguments $args, ConsoleIo $io): void protected function _parseAsPHP(ConsoleIo $io, string $functionName, array $map): void { $count = 0; - $tokenCount = \count($this->_tokens); + $tokenCount = count($this->_tokens); while ($tokenCount - $count > 1) { $countToken = $this->_tokens[$count]; @@ -302,7 +305,7 @@ protected function _searchFiles(): void */ protected function _isTwigUsable(): bool { - return class_exists(\Twig\Environment::class); + return class_exists(Environment::class); } /** @@ -333,20 +336,31 @@ protected function _tokenizeAsPHP(string $code, string $file): void */ protected function _tokenizeAsTwig(string $code, string $file): void { - $twig = new \Twig\Environment(new \Twig\Loader\ArrayLoader()); - $stream = $twig->tokenize(new \Twig\Source(code: $code, name: $file, path: $file)); + $twig = new Environment(new ArrayLoader()); + $stream = $twig->tokenize(new Source(code: $code, name: $file, path: $file)); $this->_tokens = []; while (!$stream->isEOF()) { $token = $stream->next(); - if (! $token->test(\Twig\Token::TEXT_TYPE) && ! $token->test(\Twig\Token::BLOCK_END_TYPE)) { + if (! $token->test(Token::TEXT_TYPE) && ! $token->test(Token::BLOCK_END_TYPE)) { $this->_tokens[] = $token; } } unset($stream); } - protected function _getStringFromToken(int $position, int $offset):string { + /** + * Return the string represented by a token and offset + * + * It also escapes double quotes with backslash: " -> \" + * + * @param int $position The position of the token in $this->_tokens + * @param int $offset The offset from that position. + * @return string The escaped string + */ + protected function _getStringFromToken(int $position, int $offset): string + { $string = $this->_tokens[$position + $offset]->getValue(); + return str_replace('"', '\"', $string); } } diff --git a/tests/TestCase/Command/I18nExtractCommandTest.php b/tests/TestCase/Command/I18nExtractCommandTest.php index 2ae3978..986ce3e 100644 --- a/tests/TestCase/Command/I18nExtractCommandTest.php +++ b/tests/TestCase/Command/I18nExtractCommandTest.php @@ -1,5 +1,4 @@ path . DS, [ $this->path, @@ -129,7 +128,6 @@ public function testExecute(): void $pattern .= 'msgid "no_domain_singular_with_context_with_\{var\}"\nmsgid_plural "no_domain_plural_with_context_with_\{var\}"@'; $this->assertMatchesRegularExpression($pattern, $result, 'No duplicate msgid'); - $this->assertStringContainsString('msgid "double \\"quoted\\""', $result, 'Strings with quotes not handled correctly'); $this->assertStringContainsString("msgid \"single 'quoted'\"", $result, 'Strings with quotes not handled correctly'); @@ -187,7 +185,7 @@ public function testExecuteNoOutputOption(): void 'i18n extract ' . '--merge=no ' . '--extract-core=no ' . - '--paths=' . TEST_APP . 'templates' . DS . 'i18n ' , + '--paths=' . TEST_APP . 'templates' . DS . 'i18n ', [ $this->path, TEST_APP . 'templates' . DS . 'i18n' . DS, @@ -211,7 +209,7 @@ public function testExecuteMerge(): void '--output=' . $this->path . DS, [ $this->path, - ] + ], ); $this->assertExitSuccess(); $this->assertFileExists($this->path . DS . 'default.pot'); From b280622c13b16e91df4cd48f233c8d17e87d272e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Nagy?= Date: Tue, 18 Aug 2026 22:42:23 +0200 Subject: [PATCH 4/9] Increase test coverage --- src/Command/TwigExtractCommand.php | 3 + tests/Fixture/AuthorsFixture.php | 8 +- .../Command/I18nExtractCommandTest.php | 210 +++++++++++++++++- tests/bootstrap.php | 4 +- tests/test_app/config/bootstrap.php | 2 + tests/test_app/resources/locales/.gitkeep | 0 .../src/Model/Enum/ArticleStatusExtract.php | 29 +++ tests/test_app/templates/Error/error400.php | 31 +++ tests/test_app/templates/Error/error500.php | 29 +++ tests/test_app/templates/Pages/extract.php | 37 +++ tests/test_app/templates/Pages/home.php | 173 +++++++++++++++ tests/test_app/templates/Posts/cache_form.php | 11 + 12 files changed, 526 insertions(+), 11 deletions(-) create mode 100644 tests/test_app/resources/locales/.gitkeep create mode 100644 tests/test_app/src/Model/Enum/ArticleStatusExtract.php create mode 100644 tests/test_app/templates/Error/error400.php create mode 100644 tests/test_app/templates/Error/error500.php create mode 100644 tests/test_app/templates/Pages/extract.php create mode 100644 tests/test_app/templates/Pages/home.php create mode 100644 tests/test_app/templates/Posts/cache_form.php diff --git a/src/Command/TwigExtractCommand.php b/src/Command/TwigExtractCommand.php index da63e2f..2fa511f 100644 --- a/src/Command/TwigExtractCommand.php +++ b/src/Command/TwigExtractCommand.php @@ -110,6 +110,9 @@ protected function _extractTokens(Arguments $args, ConsoleIo $io): void } } } + if ($_parser === 'php') { + $this->extractFileReflection($file, $code); + } if (!$isVerbose) { $progress->increment(1); diff --git a/tests/Fixture/AuthorsFixture.php b/tests/Fixture/AuthorsFixture.php index 78dd420..40f1eae 100644 --- a/tests/Fixture/AuthorsFixture.php +++ b/tests/Fixture/AuthorsFixture.php @@ -27,10 +27,8 @@ class AuthorsFixture extends TestFixture { /** * fields property - * - * @var array */ - public $fields = [ + public array $fields = [ 'id' => ['type' => 'integer'], 'name' => ['type' => 'string', 'default' => null], '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], @@ -38,10 +36,8 @@ class AuthorsFixture extends TestFixture /** * records property - * - * @var array */ - public $records = [ + public array $records = [ ['name' => 'mariano'], ['name' => 'nate'], ['name' => 'larry'], diff --git a/tests/TestCase/Command/I18nExtractCommandTest.php b/tests/TestCase/Command/I18nExtractCommandTest.php index 986ce3e..a3acd59 100644 --- a/tests/TestCase/Command/I18nExtractCommandTest.php +++ b/tests/TestCase/Command/I18nExtractCommandTest.php @@ -22,6 +22,7 @@ use Cake\Routing\Router; use Cake\TestSuite\TestCase; use Cake\Utility\Filesystem; +use function is_string; /** * I18nExtractCommandTest @@ -74,11 +75,8 @@ public function testExecute(): void 'i18n extract ' . '--merge=no ' . '--extract-core=no ' . - '--paths=' . TEST_APP . 'templates' . DS . 'i18n ' . DS . + '--paths=' . TEST_APP . 'templates' . DS . 'i18n ' . '--output=' . $this->path . DS, - [ - $this->path, - ], ); $this->assertExitSuccess(); $this->assertFileExists($this->path . DS . 'default.pot'); @@ -239,4 +237,208 @@ public function testExtractWithExclude(): void $pattern = '/\#: .*default\.php:\d+\n/'; $this->assertDoesNotMatchRegularExpression($pattern, $result); } + + /** + * testExtractWithoutLocations method + */ + public function testExtractWithoutLocations(): void + { + $this->exec( + 'i18n extract ' . + '--extract-core=no ' . + '--no-location=true ' . + '--exclude=Pages,Layout ' . + '--paths=' . TEST_APP . 'templates' . DS . ' ' . + '--output=' . $this->path . DS, + ); + $this->assertExitSuccess(); + $this->assertFileExists($this->path . DS . 'default.pot'); + + $result = file_get_contents($this->path . DS . 'default.pot'); + + $pattern = '/\n\#: .*\n/'; + $this->assertDoesNotMatchRegularExpression($pattern, $result); + } + + /** + * test extract can read more than one path. + */ + public function testExtractMultiplePaths(): void + { + $this->exec( + 'i18n extract ' . + '--extract-core=no ' . + '--exclude=Pages,Layout ' . + '--paths=' . TEST_APP . 'templates/Pages,' . + TEST_APP . 'templates/Posts,' . + TEST_APP . 'templates/i18n ' . + '--output=' . $this->path . DS, + ); + $this->assertExitSuccess(); + $result = file_get_contents($this->path . DS . 'default.pot'); + + $pattern = '/msgid "Add User"/'; + $this->assertMatchesRegularExpression($pattern, $result); + } + + /** + * Test that the extract shell overwrites existing files with the overwrite parameter + */ + public function testExtractOverwrite(): void + { + file_put_contents($this->path . DS . 'default.pot', 'will be overwritten'); + $this->assertFileExists($this->path . DS . 'default.pot'); + $original = file_get_contents($this->path . DS . 'default.pot'); + + $this->exec( + 'i18n extract ' . + '--extract-core=no ' . + '--overwrite ' . + '--paths=' . TEST_APP . 'templates/ ' . + '--output=' . $this->path . DS, + ); + $this->assertExitSuccess(); + + $result = file_get_contents($this->path . DS . 'default.pot'); + $this->assertNotEquals($original, $result); + } + + /** + * Test that the extract shell scans the core libs + */ + public function testExtractCore(): void + { + $this->exec( + 'i18n extract ' . + '--extract-core=yes ' . + '--paths=' . TEST_APP . '/ ' . + '--output=' . $this->path . DS, + ); + $this->assertNotNull($this->_err); + $this->assertEmpty($this->_err->messages(), 'Should not have output to stderr'); + $this->assertExitSuccess(); + + $this->assertFileExists($this->path . DS . 'cake.pot'); + $result = file_get_contents($this->path . DS . 'cake.pot'); + $this->assertTrue(is_string($result)); + + $pattern = '/#: Console\/Templates\//'; + $this->assertDoesNotMatchRegularExpression($pattern, $result); + + $pattern = '/#: Test\//'; + $this->assertDoesNotMatchRegularExpression($pattern, $result); + } + + /** + * Test when marker-error option is set + * When marker-error is unset, it's already test + * with other functions like testExecute that not detects error because err never called + */ + public function testMarkerErrorSets(): void + { + $this->exec( + 'i18n extract ' . + '--marker-error ' . + '--merge=no ' . + '--extract-core=no ' . + '--paths=' . TEST_APP . 'templates/Pages ' . + '--output=' . $this->path . DS, + ); + $this->assertExitSuccess(); + $this->assertErrorContains('Invalid marker content in'); + $this->assertErrorContains('extract.php'); + } + + /** + * Test extraction of Label attribute strings from enum cases. + */ + public function testExtractLabelAttributes(): void + { + $this->exec( + 'i18n extract ' . + '--merge=no ' . + '--extract-core=no ' . + '--paths=' . TEST_APP . 'src/Model/Enum ' . + '--output=' . $this->path . DS, + ); + $this->assertExitSuccess(); + $this->assertFileExists($this->path . DS . 'default.pot'); + $result = file_get_contents($this->path . DS . 'default.pot'); + + $this->assertStringContainsString('msgid "Published"', $result); + $this->assertStringContainsString('msgid "Unpublished"', $result); + + $pattern = '/msgctxt "article_status"\nmsgid "Archived"/'; + $this->assertMatchesRegularExpression($pattern, $result); + } + + /** + * test relative-paths option + */ + public function testExtractWithRelativePaths(): void + { + $this->exec( + 'i18n extract ' . + '--extract-core=no ' . + '--paths=' . TEST_APP . 'templates ' . + '--output=' . $this->path . DS, + ); + $this->assertExitSuccess(); + $this->assertFileExists($this->path . DS . 'default.pot'); + $result = file_get_contents($this->path . DS . 'default.pot'); + + $expected = '#: ./tests/test_app/templates/Pages/extract.php:'; + $this->assertStringContainsString($expected, $result); + } + + /** + * test invalid path options + */ + public function testExtractWithInvalidPaths(): void + { + $this->exec( + 'i18n extract ' . + '--extract-core=no ' . + '--paths=' . TEST_APP . 'templates,' . TEST_APP . 'unknown ' . + '--output=' . $this->path . DS, + ); + $this->assertExitSuccess(); + $this->assertFileExists($this->path . DS . 'default.pot'); + $result = file_get_contents($this->path . DS . 'default.pot'); + + $expected = '#: ./tests/test_app/templates/Pages/extract.php:'; + $this->assertStringContainsString($expected, $result); + } + + /** + * Test with associative arrays in App.path.locales and App.path.templates. + */ + public function testExtractWithAssociativePaths(): void + { + Configure::write('App.paths', [ + 'plugins' => ['customKey' => TEST_APP . 'plugins' . DS], + 'templates' => ['customKey' => TEST_APP . 'templates' . DS], + 'locales' => ['customKey' => TEST_APP . 'resources' . DS . 'locales' . DS], + ]); + + $this->exec( + 'i18n extract ' . + '--merge=no ' . + '--extract-core=no ', + [ + // Sending two empty inputs so \Cake\Command\I18nExtractCommand::_getPaths() + // loops through all paths + $this->path, + '', + 'D', + $this->path . DS, + ], + ); + $this->assertExitSuccess(); + $this->assertFileExists($this->path . DS . 'default.pot'); + $result = file_get_contents($this->path . DS . 'default.pot'); + + $expected = '#: ./tests/test_app/templates/Pages/extract.php:'; + $this->assertStringContainsString($expected, $result); + } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 2cd2c38..414dbef 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -29,7 +29,9 @@ require dirname(__DIR__) . '/vendor/autoload.php'; define('ROOT', dirname(__DIR__)); -define('CORE_PATH', ROOT . DS . 'vendor/cakephp/cakephp'); +define('CAKE_CORE_INCLUDE_PATH', ROOT); +define('CORE_PATH', CAKE_CORE_INCLUDE_PATH . DS); +define('CAKE', CORE_PATH . 'src' . DS); define('APP', sys_get_temp_dir()); define('TMP', sys_get_temp_dir() . '/TwigViewTmp/'); define('CACHE', sys_get_temp_dir() . '/TwigViewTmp/cache/'); diff --git a/tests/test_app/config/bootstrap.php b/tests/test_app/config/bootstrap.php index 5ff14cf..2089b0f 100644 --- a/tests/test_app/config/bootstrap.php +++ b/tests/test_app/config/bootstrap.php @@ -1,2 +1,4 @@ +

+

+ : + '{$url}'" + ) ?> +

+element('exception_stack_trace'); +endif; +?> diff --git a/tests/test_app/templates/Error/error500.php b/tests/test_app/templates/Error/error500.php new file mode 100644 index 0000000..7ac7e03 --- /dev/null +++ b/tests/test_app/templates/Error/error500.php @@ -0,0 +1,29 @@ + +

+

+ : + +

+element('auto_table_warning'); + echo $this->element('exception_stack_trace'); +endif; +?> diff --git a/tests/test_app/templates/Pages/extract.php b/tests/test_app/templates/Pages/extract.php new file mode 100644 index 0000000..a242540 --- /dev/null +++ b/tests/test_app/templates/Pages/extract.php @@ -0,0 +1,37 @@ + 10]; + +// Plural +echo __n('You have %d new message.', 'You have %d new messages.', $count); +echo __n('You deleted %d message.', 'You deleted %d messages.', $messages['count']); + +// Domain Plural +echo __dn('domain', 'You have %d new message (domain).', 'You have %d new messages (domain).', '10'); +echo __dn('domain', 'You deleted %d message (domain).', 'You deleted %d messages (domain).', $messages['count']); + +// Duplicated Message +echo __('Editing this Page'); +echo __('You have %d new message.'); + +// Contains quotes +echo __('double "quoted"'); +echo __("single 'quoted'"); + +// Contains no string like a variable or a function or ... +echo __($count); + +// Multiline +__('Hot features!' + . "\n - No Configuration:" + . ' Set-up the database and let the magic begin' + . "\n - Extremely Simple:" + . " Just look at the name...It's Cake" + . "\n - Active, Friendly Community:" + . " Join us #cakephp on IRC. We'd love to help you get started"); + +// Context +echo __x('mail', 'letter'); + +// Duplicated message with different context +echo __x('alphabet', 'letter'); diff --git a/tests/test_app/templates/Pages/home.php b/tests/test_app/templates/Pages/home.php new file mode 100644 index 0000000..1bbca64 --- /dev/null +++ b/tests/test_app/templates/Pages/home.php @@ -0,0 +1,173 @@ + +

+

+ Read the changelog +

+ + +

+ URL rewriting is not properly configured on your server. + 1) Help me configure it + 2) I don't / can't use URL rewriting +

+ + +

+=')): ?> + Your version of PHP is 5.4.3 or higher + + Your version of PHP is too low. You need PHP 5.4.3 or higher to use CakePHP. + +

+ +

+ + Your version of PHP has mbstring extension loaded. + + Your version of PHP does NOT have the mbstring extension loaded. + +

+ +

+ + Your tmp directory is writable. + + Your tmp directory is NOT writable. + +

+ +

+config() : false; +if (!empty($settings)): ?> + The Engine is being used for core caching. To change the config edit APP/Config/cache.php + + Your cache is NOT working. Please check the settings in APP/Config/cache.php + +

+ +

+ + Your datasources configuration file is present. + + + Your datasources configuration file is NOT present. +
+ Rename APP/Config/datasources.default.php to APP/Config/datasources.php +
+ +

+ + +

' + PCRE has not been compiled with Unicode support.'; +
+ Recompile PCRE with Unicode support by adding --enable-unicode-properties when configuring +

+ + +

+ + DebugKit plugin is present + + '; + DebugKit is not installed. It will help you inspect and debug different aspects of your application. +
+ You can install it from Html->link('GitHub', 'https://github.com/cakephp/debug_kit'); ?> +
+ +

+ +

Editing this Page

+

+To change the content of this page, edit: APP/View/Pages/home.ctp.
+To change its layout, edit: APP/View/Layout/default.ctp.
+You can also add some CSS styles for your pages at: APP/webroot/css.; +

+ +

Getting Started

+

+ Html->link( + 'New CakePHP Docs', + 'https://book.cakephp.org/5/en/', + ['target' => '_blank', 'escape' => false] + ); + ?> +

+

+ Html->link( + 'The 15 min Blog Tutorial', + 'https://book.cakephp.org/5/en/getting-started.html#blog-tutorial', + ['target' => '_blank', 'escape' => false] + ); + ?> +

+ +

Official Plugins

+

+

    +
  • + Html->link('DebugKit', 'https://github.com/cakephp/debug_kit') ?>: + provides a debugging toolbar and enhanced debugging tools for CakePHP application. +
  • +
  • + Html->link('Localized', 'https://github.com/cakephp/localized') ?>: + contains various localized validation classes and translations for specific countries +
  • +
+

+ +

More about CakePHP

+

+CakePHP is a rapid development framework for PHP which uses commonly known design patterns like Active Record, Association Data Mapping, Front Controller and MVC. +

+

+Our primary goal is to provide a structured framework that enables PHP users at all levels to rapidly develop robust web applications, without any loss to flexibility. +

+ + diff --git a/tests/test_app/templates/Posts/cache_form.php b/tests/test_app/templates/Posts/cache_form.php new file mode 100644 index 0000000..7100210 --- /dev/null +++ b/tests/test_app/templates/Posts/cache_form.php @@ -0,0 +1,11 @@ + +
+ Form->create(); ?> +
+ +
+ Form->submit('Submit'); ?> + Form->end(); ?> +
From 2cd3682b9cc55f2e9157883b04210caf7ad0db4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Nagy?= Date: Wed, 19 Aug 2026 10:48:47 +0200 Subject: [PATCH 5/9] Fix phpstan issues --- src/Command/TwigExtractCommand.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Command/TwigExtractCommand.php b/src/Command/TwigExtractCommand.php index 2fa511f..0c1a5fc 100644 --- a/src/Command/TwigExtractCommand.php +++ b/src/Command/TwigExtractCommand.php @@ -17,7 +17,7 @@ namespace Cake\TwigView\Command; -use Cake\Command\Helper\ProgressHelper; +use Cake\Console\Helper\ProgressHelper; use Cake\Command\I18nExtractCommand; use Cake\Console\Arguments; use Cake\Console\ConsoleIo; @@ -60,7 +60,7 @@ public static function getDescription(): string */ protected function _extractTokens(Arguments $args, ConsoleIo $io): void { - $progress = $io->helper('progress'); + $progress = $io->helper('Progress'); assert($progress instanceof ProgressHelper); $progress->init(['total' => count($this->_files)]); $isVerbose = $args->getOption('verbose'); @@ -197,6 +197,7 @@ protected function _parseAsTwig(ConsoleIo $io, string $functionName, array $map) /** @var \Twig\Token $token */ foreach ($this->_tokens as $count => $token) { if ($token->test(Token::NAME_TYPE, $functionName)) { + $singular = ''; switch ($functionName) { case '__': $singular = $this->_getStringFromToken($count, 2); @@ -245,7 +246,7 @@ protected function _parseAsTwig(ConsoleIo $io, string $functionName, array $map) if (isset($plural)) { $details['msgid_plural'] = $plural; } else { - $this->_markerError($io, $this->_file, $token->getLine(), $functionName, $token->getOffset()); + $this->_markerError($io, $this->_file, $token->getLine(), $functionName, $token->getOffset() ?? 0); continue; } } @@ -254,7 +255,7 @@ protected function _parseAsTwig(ConsoleIo $io, string $functionName, array $map) if (isset($context)) { $details['msgctxt'] = $context; } else { - $this->_markerError($io, $this->_file, $token->getLine(), $functionName, $token->getOffset()); + $this->_markerError($io, $this->_file, $token->getLine(), $functionName, $token->getOffset() ?? 0); continue; } } @@ -366,4 +367,14 @@ protected function _getStringFromToken(int $position, int $offset): string return str_replace('"', '\"', $string); } + + /** + * Adding this here to fix a PHP 8.2 error in the tests + * + * @return void + */ + protected function extractFileReflection(string $file, string $code): void + { + parent::extractFileReflection($file, $code); + } } From dacbfb02f0e61a24ac9b420f3e7d76c5dd02b42b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Nagy?= Date: Wed, 19 Aug 2026 11:39:27 +0200 Subject: [PATCH 6/9] Change the order of use statements. --- src/Command/TwigExtractCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Command/TwigExtractCommand.php b/src/Command/TwigExtractCommand.php index 0c1a5fc..e91dc65 100644 --- a/src/Command/TwigExtractCommand.php +++ b/src/Command/TwigExtractCommand.php @@ -17,10 +17,10 @@ namespace Cake\TwigView\Command; -use Cake\Console\Helper\ProgressHelper; use Cake\Command\I18nExtractCommand; use Cake\Console\Arguments; use Cake\Console\ConsoleIo; +use Cake\Console\Helper\ProgressHelper; use Cake\Utility\Filesystem; use Twig\Environment; use Twig\Loader\ArrayLoader; From e7130f88b59c47469997d93152134154edd43e5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Nagy?= Date: Wed, 19 Aug 2026 11:40:05 +0200 Subject: [PATCH 7/9] Correct some additional rector issues not connected to my changes. --- src/Filesystem/RelativeScanner.php | 1 - src/Twig/FileLoader.php | 1 - src/View/TwigView.php | 3 --- tests/TestCase/Twig/Extension/AbstractExtensionTest.php | 5 ----- tests/test_app/templates/Pages/home.php | 4 ---- 5 files changed, 14 deletions(-) diff --git a/src/Filesystem/RelativeScanner.php b/src/Filesystem/RelativeScanner.php index dce3609..ef860da 100644 --- a/src/Filesystem/RelativeScanner.php +++ b/src/Filesystem/RelativeScanner.php @@ -44,7 +44,6 @@ public static function all(array $extensions): array * * @param string $plugin The plugin to find all templates for. * @param array $extensions Template extensions to search - * @return mixed */ public static function plugin(string $plugin, array $extensions): mixed { diff --git a/src/Twig/FileLoader.php b/src/Twig/FileLoader.php index 6bc1d60..f111ba6 100644 --- a/src/Twig/FileLoader.php +++ b/src/Twig/FileLoader.php @@ -160,7 +160,6 @@ protected function loaderError(string $name, array $templatePaths): LoaderError * which file exists. * * @param string $partial Template path excluding extension - * @return string|null */ public function checkExtensions(string $partial): ?string { diff --git a/src/View/TwigView.php b/src/View/TwigView.php index cc22e43..af1e9d1 100644 --- a/src/View/TwigView.php +++ b/src/View/TwigView.php @@ -127,8 +127,6 @@ public function getTwig(): Environment /** * Gets Twig Profile if profiler enabled. - * - * @return \Twig\Profiler\Profile|null */ public function getProfile(): ?Profile { @@ -241,7 +239,6 @@ public function __construct(MarkdownInterface $engine) /** * @param string $class FQCN - * @return object|null */ public function load(string $class): ?object { diff --git a/tests/TestCase/Twig/Extension/AbstractExtensionTest.php b/tests/TestCase/Twig/Extension/AbstractExtensionTest.php index 9dd8cf8..c8b097b 100644 --- a/tests/TestCase/Twig/Extension/AbstractExtensionTest.php +++ b/tests/TestCase/Twig/Extension/AbstractExtensionTest.php @@ -38,11 +38,6 @@ protected function setUp(): void } } - protected function tearDown(): void - { - parent::tearDown(); - } - public function testGetTokenParsers(): void { $tokenParsers = $this->extension->getTokenParsers(); diff --git a/tests/test_app/templates/Pages/home.php b/tests/test_app/templates/Pages/home.php index 1bbca64..0224078 100644 --- a/tests/test_app/templates/Pages/home.php +++ b/tests/test_app/templates/Pages/home.php @@ -25,11 +25,7 @@

-=')): ?> Your version of PHP is 5.4.3 or higher - - Your version of PHP is too low. You need PHP 5.4.3 or higher to use CakePHP. -

From 389efb7f08d461fb7b7994c6e03850b28607402d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Nagy?= Date: Wed, 19 Aug 2026 12:07:23 +0200 Subject: [PATCH 8/9] Use FQCN For ProgressHelper as it has an alias --- src/Command/TwigExtractCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Command/TwigExtractCommand.php b/src/Command/TwigExtractCommand.php index e91dc65..154748e 100644 --- a/src/Command/TwigExtractCommand.php +++ b/src/Command/TwigExtractCommand.php @@ -61,7 +61,7 @@ public static function getDescription(): string protected function _extractTokens(Arguments $args, ConsoleIo $io): void { $progress = $io->helper('Progress'); - assert($progress instanceof ProgressHelper); + assert($progress instanceof \Cake\Console\Helper\ProgressHelper); $progress->init(['total' => count($this->_files)]); $isVerbose = $args->getOption('verbose'); From dfaeaba6d75f35185c159fb9747e33770593bdc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Nagy?= Date: Thu, 27 Aug 2026 16:21:38 +0200 Subject: [PATCH 9/9] remove FQDN from reference --- src/Command/TwigExtractCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Command/TwigExtractCommand.php b/src/Command/TwigExtractCommand.php index 154748e..e91dc65 100644 --- a/src/Command/TwigExtractCommand.php +++ b/src/Command/TwigExtractCommand.php @@ -61,7 +61,7 @@ public static function getDescription(): string protected function _extractTokens(Arguments $args, ConsoleIo $io): void { $progress = $io->helper('Progress'); - assert($progress instanceof \Cake\Console\Helper\ProgressHelper); + assert($progress instanceof ProgressHelper); $progress->init(['total' => count($this->_files)]); $isVerbose = $args->getOption('verbose');