From d24a58184356a8b38e524826c62431d7f2f27cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Mystik=20Jon=C3=A1=C5=A1?= Date: Thu, 14 Jul 2022 11:01:07 +0200 Subject: [PATCH] Latte 3 support (#106) --- .github/workflows/main.yaml | 13 ++++- Makefile | 3 + composer.json | 9 ++- phpstan.lowest.neon | 71 +++++++++++++++++++++++ phpstan.neon | 11 ++++ src/DI/TranslationExtension.php | 17 ++++-- src/Helpers.php | 29 ++++++---- src/Latte/Macros.php | 14 ++++- src/Latte/Nodes/TranslateNode.php | 91 ++++++++++++++++++++++++++++++ src/Latte/Nodes/TranslatorNode.php | 64 +++++++++++++++++++++ src/Latte/TranslatorExtension.php | 90 +++++++++++++++++++++++++++++ tests/Tests/TranslatorTest.phpt | 26 +++++++-- 12 files changed, 411 insertions(+), 27 deletions(-) create mode 100644 phpstan.lowest.neon create mode 100644 src/Latte/Nodes/TranslateNode.php create mode 100644 src/Latte/Nodes/TranslatorNode.php create mode 100644 src/Latte/TranslatorExtension.php diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 34b8c4b..75fdff9 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -74,6 +74,15 @@ jobs: name: "Static analysis" runs-on: "ubuntu-latest" + strategy: + matrix: + composer-args: [ "" ] + command: [ "phpstan" ] + include: + - composer-args: "--prefer-lowest" + command: "phpstan-lowest" + fail-fast: false + steps: - name: "Checkout" uses: "actions/checkout@v2" @@ -115,10 +124,10 @@ jobs: restore-keys: "${{ runner.os }}-composer-" - name: "Install dependencies" - run: "${{ env.composer-install }}" + run: "${{ env.composer-install }} ${{ matrix.composer-args }}" - name: "PHPStan" - run: "make phpstan" + run: "make ${{ matrix.command}}" tests: name: "Tests" diff --git a/Makefile b/Makefile index 844e78c..fc4c02c 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,9 @@ csf: vendor phpstan: vendor vendor/bin/phpstan analyse -l max -c phpstan.neon src +phpstan-lowest: vendor + vendor/bin/phpstan analyse -l max -c phpstan.lowest.neon src + tests: vendor vendor/bin/tester -s -p php --colors 1 -C tests/Tests diff --git a/composer.json b/composer.json index 519e3b6..6a6ddf2 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ ], "require": { "php": "^8.0.2", - "latte/latte": "^2.6", + "latte/latte": "^2.6|^3.0", "nette/di": "^3.0.6", "nette/finder": "^2.5.2", "nette/http": "^3.0", @@ -37,7 +37,7 @@ "nette/tester": "^2.3.1", "ninjify/nunjuck": "^0.3.0", "ninjify/qa": "^0.13", - "phpstan/phpstan": "^1.4", + "phpstan/phpstan": "^1.8", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-nette": "^1.0", "phpstan/phpstan-strict-rules": "^1.1", @@ -55,7 +55,10 @@ } }, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } }, "extra": { "branch-alias": { diff --git a/phpstan.lowest.neon b/phpstan.lowest.neon new file mode 100644 index 0000000..c348c4e --- /dev/null +++ b/phpstan.lowest.neon @@ -0,0 +1,71 @@ +includes: + - vendor/phpstan/phpstan-deprecation-rules/rules.neon + - vendor/phpstan/phpstan-nette/extension.neon + - vendor/phpstan/phpstan-nette/rules.neon + - vendor/phpstan/phpstan-strict-rules/rules.neon + +parameters: + level: max + paths: + - src + - tests + excludePaths: + - src/Latte/TranslatorExtension.php + - src/Latte/Nodes/* + ignoreErrors: + - + count: 2 + message: '#^Variable property access on object\.$#' + path: 'src/Loaders/Doctrine.php' + + # ------------------------------------------------------------------- + # for back compatibility with old packages - will be remove in future + # ------------------------------------------------------------------- + + - + message: """ + #^Fetching class constant class of deprecated class Nette\\\\Localization\\\\ITranslator\\: + use Nette\\\\Localization\\\\Translator$# + """ + count: 2 + path: src/DI/TranslationExtension.php + + - + count: 1 + message: """ + #^Parameter \\$translator of method Contributte\\\\Translation\\\\Latte\\\\Filters\\:\\:__construct\\(\\) has typehint with deprecated interface Nette\\\\Localization\\\\ITranslator\\: + use Nette\\\\Localization\\\\Translator$# + """ + path: src/Latte/Filters.php + + - + count: 1 + message: """ + #^Call to an undefined method Nette\\\\Localization\\\\ITranslator::translate\\(\\)# + """ + path: src/Latte/Filters.php + + - + count: 1 + message: """ + #^Parameter \\$translator of method Contributte\\\\Translation\\\\Latte\\\\TranslatorExtension\\:\\:__construct\\(\\) has typehint with deprecated interface Nette\\\\Localization\\\\ITranslator\\: + use Nette\\\\Localization\\\\Translator$# + """ + path: src/Latte/TranslatorExtension.php + + - + count: 1 + message: """ + #^Class Contributte\\\\Translation\\\\PrefixedTranslator implements deprecated interface Nette\\\\Localization\\\\ITranslator\\: + use Nette\\\\Localization\\\\Translator$# + """ + path: src/PrefixedTranslator.php + + - + count: 1 + message: """ + #^Class Contributte\\\\Translation\\\\Translator implements deprecated interface Nette\\\\Localization\\\\ITranslator\\: + use Nette\\\\Localization\\\\Translator$# + """ + path: src/Translator.php + diff --git a/phpstan.neon b/phpstan.neon index 1fc4c30..4bb67f3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,6 +9,9 @@ parameters: paths: - src - tests + excludePaths: + - src/Latte/Filters.php + - src/Latte/Macros.php ignoreErrors: - count: 2 @@ -57,6 +60,14 @@ parameters: """ path: src/Latte/Filters.php + - + count: 1 + message: """ + #^Parameter \\$translator of method Contributte\\\\Translation\\\\Latte\\\\TranslatorExtension\\:\\:__construct\\(\\) has typehint with deprecated interface Nette\\\\Localization\\\\ITranslator\\: + use Nette\\\\Localization\\\\Translator$# + """ + path: src/Latte/TranslatorExtension.php + - count: 1 message: """ diff --git a/src/DI/TranslationExtension.php b/src/DI/TranslationExtension.php index 0476fec..4667d6c 100644 --- a/src/DI/TranslationExtension.php +++ b/src/DI/TranslationExtension.php @@ -7,6 +7,7 @@ use Contributte\Translation\Helpers; use Contributte\Translation\Latte\Filters; use Contributte\Translation\Latte\Macros; +use Contributte\Translation\Latte\TranslatorExtension; use Contributte\Translation\Loaders\Neon; use Contributte\Translation\LocaleResolver; use Contributte\Translation\LocalesResolvers\Header; @@ -238,10 +239,18 @@ public function beforeCompile(): void /** @var \Nette\DI\Definitions\FactoryDefinition $latteFactory */ $latteFactory = $builder->getDefinition($latteFactoryName); - $latteFactory->getResultDefinition() - ->addSetup('?->onCompile[] = function (Latte\\Engine $engine): void { ?::install($engine->getCompiler()); }', ['@self', new PhpLiteral(Macros::class)]) - ->addSetup('addProvider', ['translator', $iTranslator]) - ->addSetup('addFilter', ['translate', [$latteFilters, 'translate']]); + /** @phpstan-ignore-next-line */ + if (version_compare(\Latte\Engine::VERSION, '3', '<')) { + $latteFactory->getResultDefinition() + ->addSetup('?->onCompile[] = function (Latte\\Engine $engine): void { ?::install($engine->getCompiler()); }', ['@self', new PhpLiteral(Macros::class)]) + ->addSetup('addProvider', ['translator', $iTranslator]) + ->addSetup('addFilter', ['translate', [$latteFilters, 'translate']]); + } else { + $latteExtension = $builder->addDefinition($this->prefix('latte.extension')) + ->setFactory(TranslatorExtension::class); + $latteFactory->getResultDefinition() + ->addSetup('addExtension', [$latteExtension]); + } } /** @var \Contributte\Translation\DI\TranslationProviderInterface $v1 */ diff --git a/src/Helpers.php b/src/Helpers.php index 855bc83..0932b6a 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -2,7 +2,6 @@ namespace Contributte\Translation; -use Latte\MacroNode; use Nette\Utils\Strings; class Helpers @@ -38,15 +37,6 @@ public static function extractMessage( return [$domain, $message]; } - public static function macroWithoutParameters( - MacroNode $node - ): bool - { - $result = Strings::trim($node->tokenizer->joinUntil(',')) === Strings::trim($node->args); - $node->tokenizer->reset(); - return $result; - } - public static function isAbsoluteMessage( string $message ): bool @@ -54,11 +44,28 @@ public static function isAbsoluteMessage( return Strings::startsWith($message, '//'); } + /** + * @param mixed $message + * @param array|null $prefix + * @return mixed + */ + public static function prefixMessage( + $message, + ?array $prefix + ) + { + if (is_string($message) && $prefix !== null && !self::isAbsoluteMessage($message)) { + $message = implode('.', $prefix) . '.' . $message; + } + + return $message; + } + public static function createLatteProperty( string $suffix ): string { - return '$ʟ_contributteTranslation' . $suffix; + return '$ᴛ_contributteTranslation' . $suffix; } } diff --git a/src/Latte/Macros.php b/src/Latte/Macros.php index fde7031..87c3c2c 100644 --- a/src/Latte/Macros.php +++ b/src/Latte/Macros.php @@ -9,6 +9,7 @@ use Latte\MacroNode; use Latte\Macros\MacroSet; use Latte\PhpWriter; +use Nette\Utils\Strings; class Macros extends MacroSet { @@ -50,8 +51,10 @@ public function macroTranslate( $value = 'ob_get_clean()'; } + /** @phpstan-ignore-next-line */ if (!defined(Engine::class . '::VERSION_ID') || Engine::VERSION_ID < 20900) { $latteProp = '$_fi'; + /** @phpstan-ignore-next-line */ } elseif (Engine::VERSION_ID >= 20900 && Engine::VERSION_ID < 20902) { $latteProp = '$__fi'; } else { @@ -65,7 +68,7 @@ public function macroTranslate( $messageProp = Helpers::createLatteProperty('Message'); $prefixProp = Helpers::createLatteProperty('Prefix'); - $macroCodeEcho = Helpers::macroWithoutParameters($node) + $macroCodeEcho = self::macroWithoutParameters($node) ? sprintf('echo %%modify(call_user_func($this->filters->translate, %s))', $messageProp) : sprintf('echo %%modify(call_user_func($this->filters->translate, %s, %%node.args))', $messageProp); @@ -121,4 +124,13 @@ public function macroPrefix( ', $tempPrefixProp, $tempPrefixProp, $prefixProp, $tempPrefixProp, $prefixProp, $prefixProp)); } + public static function macroWithoutParameters( + MacroNode $node + ): bool + { + $result = Strings::trim($node->tokenizer->joinUntil(',')) === Strings::trim($node->args); + $node->tokenizer->reset(); + return $result; + } + } diff --git a/src/Latte/Nodes/TranslateNode.php b/src/Latte/Nodes/TranslateNode.php new file mode 100644 index 0000000..8c370ad --- /dev/null +++ b/src/Latte/Nodes/TranslateNode.php @@ -0,0 +1,91 @@ +, array{AreaNode, ?Tag}, TranslateNode|NopNode> */ + public static function create( + Tag $tag + ): \Generator + { + $tag->outputMode = $tag::OutputKeepIndentation; + + $node = new TranslateNode(); + $args = $tag->parser->parseArguments(); + $node->modifier = $tag->parser->parseModifier(); + $node->modifier->escape = true; + if ($tag->void) { + return new NopNode(); + } + + [$node->content] = yield; + + if (($text = NodeHelpers::toText($node->content)) !== null) { + $node->content = new TextNode($text); + } + + array_unshift($node->modifier->filters, new Php\FilterNode(new Php\IdentifierNode('translate'), $args->toArguments())); + + return $node; + } + + + public function print( + PrintContext $context + ): string + { + if ($this->content instanceof TextNode) { + return $context->format( + ' + $ʟ_fi = new LR\FilterInfo(%dump); + echo %modifyContent(%dump) %line; + ', + $context->getEscaper()->export(), + $this->modifier, + $this->content->content, + $this->position, + ); + + } else { + return $context->format( + ' + ob_start(fn() => ""); try { + %node + } finally { + $ʟ_tmp = ob_get_clean(); + } + $ʟ_fi = new LR\FilterInfo(%dump); + echo %modifyContent($ʟ_tmp) %line; + ', + $this->content, + $context->getEscaper()->export(), + $this->modifier, + $this->position, + ); + } + } + + + public function &getIterator(): \Generator + { + yield $this->content; + yield $this->modifier; + } + +} diff --git a/src/Latte/Nodes/TranslatorNode.php b/src/Latte/Nodes/TranslatorNode.php new file mode 100644 index 0000000..4364dee --- /dev/null +++ b/src/Latte/Nodes/TranslatorNode.php @@ -0,0 +1,64 @@ +, array{AreaNode, ?Tag}, TranslatorNode> */ + public static function create( + Tag $tag + ): \Generator + { + $tag->expectArguments(); + $variable = $tag->parser->parseUnquotedStringOrExpression(); + + $node = new TranslatorNode(); + $node->prefix = $variable; + [$node->content] = yield; + return $node; + } + + + public function print( + PrintContext $context + ): string + { + $prefixProp = Helpers::createLatteProperty('Prefix'); + $tempPrefixProp = Helpers::createLatteProperty('TempPrefix'); + + return $context->format( + sprintf(' + %s = %s ?? [] %%line; + array_push(%s, %s ?? null); + %s = [%%node]; + + %%node + + %s = array_pop(%s); + ', $tempPrefixProp, $tempPrefixProp, $tempPrefixProp, $prefixProp, $prefixProp, $prefixProp, $tempPrefixProp), + $this->position, + $this->prefix, + $this->content + ); + } + + + public function &getIterator(): \Generator + { + yield $this->prefix; + yield $this->content; + } + +} diff --git a/src/Latte/TranslatorExtension.php b/src/Latte/TranslatorExtension.php new file mode 100644 index 0000000..10ac6e5 --- /dev/null +++ b/src/Latte/TranslatorExtension.php @@ -0,0 +1,90 @@ +translator = $translator; + } + + public function getTags(): array + { + return [ + '_' => [$this, 'parseTranslate'], + 'translate' => [TranslateNode::class, 'create'], + 'translator' => [TranslatorNode::class, 'create'], + ]; + } + + public function getFilters(): array + { + return [ + 'translate' => fn(FilterInfo $fi, ...$args): string => $this->translator->translate(...$args), + ]; + } + + public function getProviders(): array + { + return [ + 'translator' => $this->translator, + ]; + } + + public function parseTranslate( + Tag $tag + ): Node + { + $tag->outputMode = $tag::OutputKeepIndentation; + $tag->expectArguments(); + $expression = $tag->parser->parseUnquotedStringOrExpression(); + $args = new ArrayNode(); + if ($tag->parser->stream->tryConsume(',') !== null) { + $args = $tag->parser->parseArguments(); + } + + $prefixProp = Helpers::createLatteProperty('Prefix'); + + $messageNode = new StaticCallNode( + new NameNode('\Contributte\Translation\Helpers', NameNode::KindFullyQualified), + new IdentifierNode('prefixMessage'), + [ + new ArgumentNode($expression), + new ArgumentNode(new BinaryOpNode(new VariableNode(substr($prefixProp, 1)), '??', new NullNode())), + ] + ); + + $outputNode = new PrintNode(); + $outputNode->modifier = $tag->parser->parseModifier(); + $outputNode->modifier->escape = true; + $outputNode->expression = $messageNode; + array_unshift($outputNode->modifier->filters, new FilterNode(new IdentifierNode('translate'), $args->toArguments())); + return $outputNode; + } + +} diff --git a/tests/Tests/TranslatorTest.phpt b/tests/Tests/TranslatorTest.phpt index d2c921e..cb0b6bd 100644 --- a/tests/Tests/TranslatorTest.phpt +++ b/tests/Tests/TranslatorTest.phpt @@ -230,18 +230,12 @@ final class TranslatorTest extends TestAbstract $translator = $container->getByType(ITranslator::class); Assert::same('Hello', $latte->renderToString(FileMock::create('{_messages.hello}'))); - Assert::same('Hello', $latte->renderToString(FileMock::create('{_}messages.hello{/_}'))); - Assert::same('Hello', $latte->renderToString(FileMock::create('{_}{php $message = "messages.hello"}{$message}{/_}'))); Assert::same('Hello', $latte->renderToString(FileMock::create('{php $message = "messages.hello"}{$message|translate}'))); Assert::same('Hello', $latte->renderToString(FileMock::create('{_hello}'))); - Assert::same('Hello', $latte->renderToString(FileMock::create('{_}hello{/_}'))); - Assert::same('Hello', $latte->renderToString(FileMock::create('{_}{php $message = "hello"}{$message}{/_}'))); Assert::same('Hello', $latte->renderToString(FileMock::create('{php $message = "hello"}{$message|translate}'))); Assert::same('Hello', $latte->renderToString(FileMock::create('{_//messages.hello}'))); - Assert::same('Hello', $latte->renderToString(FileMock::create('{_}//messages.hello{/_}'))); - Assert::same('Hello', $latte->renderToString(FileMock::create('{_}{php $message = "//messages.hello"}{$message}{/_}'))); Assert::same('Hello', $latte->renderToString(FileMock::create('{php $message = "//messages.hello"}{$message|translate}'))); Assert::same('Hello', $latte->renderToString(FileMock::create('{_hello, [], messages, en}'))); @@ -250,6 +244,26 @@ final class TranslatorTest extends TestAbstract Assert::same('Hello', $latte->renderToString(FileMock::create('{_hello, null, [], messages, en}'))); Assert::same('Hello', $latte->renderToString(FileMock::create('{php $message = "hello"}{$message|translate: null, [], messages, en}'))); + if (version_compare(\Latte\Engine::VERSION, '3', '<')) { + Assert::same('Hello', $latte->renderToString(FileMock::create('{_}messages.hello{/_}'))); + Assert::same('Hello', $latte->renderToString(FileMock::create('{_}{php $message = "messages.hello"}{$message}{/_}'))); + + Assert::same('Hello', $latte->renderToString(FileMock::create('{_}hello{/_}'))); + Assert::same('Hello', $latte->renderToString(FileMock::create('{_}{php $message = "hello"}{$message}{/_}'))); + + Assert::same('Hello', $latte->renderToString(FileMock::create('{_}//messages.hello{/_}'))); + Assert::same('Hello', $latte->renderToString(FileMock::create('{_}{php $message = "//messages.hello"}{$message}{/_}'))); + } else { + Assert::same('Hello', $latte->renderToString(FileMock::create('{translate}messages.hello{/translate}'))); + Assert::same('Hello', $latte->renderToString(FileMock::create('{translate}{php $message = "messages.hello"}{$message}{/translate}'))); + + Assert::same('Hello', $latte->renderToString(FileMock::create('{translate}hello{/translate}'))); + Assert::same('Hello', $latte->renderToString(FileMock::create('{translate}{php $message = "hello"}{$message}{/translate}'))); + + Assert::same('Hello', $latte->renderToString(FileMock::create('{translate}//messages.hello{/translate}'))); + Assert::same('Hello', $latte->renderToString(FileMock::create('{translate}{php $message = "//messages.hello"}{$message}{/translate}'))); + } + Assert::same('Hi Ales!', $latte->renderToString(FileMock::create('{_messages.hi, [name => Ales]}'))); Assert::same('Hi Ales!', $latte->renderToString(FileMock::create('{php $message = "messages.hi"}{$message|translate: [name => Ales]}')));