diff --git a/src/Latte/Compiler/Compiler.php b/src/Latte/Compiler/Compiler.php index b8a43ea78..2e1f01235 100644 --- a/src/Latte/Compiler/Compiler.php +++ b/src/Latte/Compiler/Compiler.php @@ -50,9 +50,12 @@ class Compiler /** @var string[] @internal */ public $placeholders = []; - /** @var string|null */ + /** @var string */ public $paramsExtraction; + /** @var string */ + private $defaultParamsExtraction = 'extract($this->params);'; + /** @var Token[] */ private $tokens; @@ -166,7 +169,8 @@ private function buildClassBody(array $tokens): string $output = ''; $this->output = &$output; $this->inHead = true; - $this->htmlNode = $this->macroNode = $this->context = $this->paramsExtraction = null; + $this->htmlNode = $this->macroNode = $this->context = null; + $this->paramsExtraction = $this->defaultParamsExtraction; $this->placeholders = $this->properties = $this->constants = []; $this->methods = ['main' => null, 'prepare' => null]; @@ -215,7 +219,7 @@ private function buildClassBody(array $tokens): string $epilogs = (empty($res[1]) ? '' : "") . $epilogs; } - $extractParams = $this->paramsExtraction ?? 'extract($this->params);'; + $extractParams = $this->paramsExtraction; $this->addMethod('main', $this->expandTokens($extractParams . "?>\n$output$epilogstokenizer; $params = []; while ($tokens->isNext()) { - if ($tokens->nextToken($tokens::T_SYMBOL, '?', 'null', '\\')) { // type - $tokens->nextAll($tokens::T_SYMBOL, '\\', '|', '[', ']', 'null'); + $type = $tokens->nextValue($tokens::T_SYMBOL, '?', 'null', '\\'); + if ($type) { + $type .= $tokens->joinAll($tokens::T_SYMBOL, '\\', '|', '[', ']', 'null'); } $param = $tokens->consumeValue($tokens::T_VARIABLE); $default = $tokens->nextToken('=') ? $tokens->joinUntilSameDepth(',') : 'null'; + $mask ='%raw = $ʟ_args[%var] ?? $ʟ_args[%var] ?? %raw;'; + if($type) { + $mask = "/** @var $type $param */\n" . $mask; + } $params[] = $writer->write( - '%raw = $ʟ_args[%var] ?? $ʟ_args[%var] ?? %raw;', + $mask, $param, count($params), substr($param, 1), @@ -556,7 +561,7 @@ private function addBlock(MacroNode $node, string $layer = null): Block private function extractMethod(MacroNode $node, Block $block, string $params = null): void { if (preg_match('#\$|n:#', $node->content)) { - $node->content = 'name === 'block' && $node->closest(['embed']) ? 'end($this->varStack)' : '$this->params') . ');' + $node->content = 'name === 'block' && $node->closest(['embed']) ? 'extract(end($this->varStack));' : $this->getCompiler()->paramsExtraction) . ($params ?? 'extract($ʟ_args);') . 'unset($ʟ_args);?>' . $node->content; diff --git a/src/Latte/Macros/CoreMacros.php b/src/Latte/Macros/CoreMacros.php index b0e588319..8339c70ae 100644 --- a/src/Latte/Macros/CoreMacros.php +++ b/src/Latte/Macros/CoreMacros.php @@ -17,7 +17,6 @@ use Latte\PhpHelpers; use Latte\PhpWriter; - /** * Basic macros for Latte. */ @@ -813,15 +812,20 @@ public function macroParameters(MacroNode $node, PhpWriter $writer): void $tokens = $node->tokenizer; $params = []; while ($tokens->isNext()) { - if ($tokens->nextToken($tokens::T_SYMBOL, '?', 'null', '\\')) { // type - $tokens->nextAll($tokens::T_SYMBOL, '\\', '|', '[', ']', 'null'); + $type = $tokens->nextValue($tokens::T_SYMBOL, '?', 'null', '\\'); + if ($type) { + $type .= $tokens->joinAll($tokens::T_SYMBOL, '\\', '|', '[', ']', 'null'); } $param = $tokens->consumeValue($tokens::T_VARIABLE); $default = $tokens->nextToken('=') ? $tokens->joinUntilSameDepth(',') : 'null'; + $mask ='%raw = $this->params[%var] ?? $this->params[%var] ?? %raw;'; + if($type) { + $mask = "/** @var $type $param */\n" . $mask; + } $params[] = $writer->write( - '%raw = $this->params[%var] ?? $this->params[%var] ?? %raw;', + $mask, $param, count($params), substr($param, 1), @@ -838,7 +842,7 @@ public function macroParameters(MacroNode $node, PhpWriter $writer): void /** * {varType type $var} */ - public function macroVarType(MacroNode $node): void + public function macroVarType(MacroNode $node, PhpWriter $writer): string { if ($node->modifiers) { $node->setArgs($node->args . $node->modifiers); @@ -847,10 +851,17 @@ public function macroVarType(MacroNode $node): void $node->validate(true); $type = trim($node->tokenizer->joinUntil($node->tokenizer::T_VARIABLE)); - $variable = $node->tokenizer->nextToken($node->tokenizer::T_VARIABLE); + $variable = $node->tokenizer->nextValue($node->tokenizer::T_VARIABLE); if (!$type || !$variable) { throw new CompileException('Unexpected content, expecting {varType type $var}.'); } + $comment = "/** @var $type $variable */\n"; + if ($this->getCompiler()->isInHead()) { + $this->getCompiler()->paramsExtraction .= $comment; + return ""; + } else { + return $writer->write($comment); + } } @@ -869,12 +880,38 @@ public function macroVarPrint(MacroNode $node): string /** * {templateType ClassName} */ - public function macroTemplateType(MacroNode $node): void + public function macroTemplateType(MacroNode $node) { if (!$this->getCompiler()->isInHead()) { throw new CompileException($node->getNotation() . ' is allowed only in template header.'); } $node->validate('class name'); + try { + $reflectionClass = new \ReflectionClass($node->args); + foreach ($reflectionClass->getProperties() as $property) { + if(!$property->isPublic()) { + continue; + } + $propertyName = $property->getName(); + $type = $property->getType(); + $typeName = null; + if ($type instanceof \ReflectionNamedType) { + $typeName = ($type->allowsNull() ? "?" : "") . $type->getName(); + } elseif ($type instanceof \ReflectionUnionType) { + $typeName = implode("|", array_map( + function(\ReflectionNamedType $type) { return $type->getName(); }, + $type->getTypes() + )); + } + if(!$typeName) { + $typeName = "mixed"; + } + $comment = "/** @var $typeName \$$propertyName ({$node->args}) */\n"; + $this->getCompiler()->paramsExtraction .= $comment; + } + } catch (\ReflectionException $e) { + + } } diff --git a/tests/Latte/BlockMacros.define.args.phpt b/tests/Latte/BlockMacros.define.args.phpt index bbf1f37b0..13765c83f 100644 --- a/tests/Latte/BlockMacros.define.args.phpt +++ b/tests/Latte/BlockMacros.define.args.phpt @@ -157,3 +157,26 @@ Assert::matchFile( __DIR__ . '/expected/BlockMacros.define.args5.html', $latte->renderToString($template) ); + +// types +$latte->setLoader(new Latte\Loaders\StringLoader); +$template = <<<'XX' +default values + +{define test $var1 = 0, array $var2 = [1, 2, 3], int $var3 = 10} + Variables {$var1}, {$var2|implode}, {$var3} +{/define} + +a) {include test, 1} + +b) {include test, var1 => 1} +XX; + +Assert::matchFile( + __DIR__ . '/expected/BlockMacros.define.args6.phtml', + $latte->compile($template) +); +Assert::matchFile( + __DIR__ . '/expected/BlockMacros.define.args6.html', + $latte->renderToString($template) +); diff --git a/tests/Latte/CoreMacros.parameters.phpt b/tests/Latte/CoreMacros.parameters.phpt index d30ec5381..787936950 100644 --- a/tests/Latte/CoreMacros.parameters.phpt +++ b/tests/Latte/CoreMacros.parameters.phpt @@ -19,17 +19,28 @@ $latte->setLoader(new Latte\Loaders\StringLoader([ 'main3' => '{include inc3.latte, a: 10}', 'main4' => '{include inc4.latte, a: 10}', 'main5' => '{include inc5.latte, a: 10}', + 'main6' => '{include inc6.latte, a: 10}', + 'main7' => '{include inc7.latte, a: 10}', + 'main8' => '{include inc8.latte, a: 10}', 'inc1.latte' => '{$a ?? "-"} {$b ?? "-"} {$glob ?? "-"}', 'inc2.latte' => '{parameters $a} {$a ?? "-"} {$b ?? "-"} {$glob ?? "-"}', 'inc3.latte' => '{parameters int $a = 5} {$a ?? "-"} {$b ?? "-"} {$glob ?? "-"}', 'inc4.latte' => '{parameters $a, int $b = 5} {$a ?? "-"} {$b ?? "-"} {$glob ?? "-"}', 'inc5.latte' => '{parameters $glob} {$a ?? "-"} {$b ?? "-"} {$glob ?? "-"}', + 'inc6.latte' => '{parameters ?\Exception $glob} {$a ?? "-"} {$b ?? "-"} {$glob->getMessage() ?? "-"}', + 'inc7.latte' => '{parameters $a, int $b = 5} {block x}{$a ?? "-"} {$b ?? "-"} {$glob ?? "-"}{/block}', + 'inc8.latte' => '{parameters $a, int $b = 5} {define x}{$a ?? "-"} {$b ?? "-"} {$glob ?? "-"}{/define}{include x}', ])); - Assert::same('10 - 123', $latte->renderToString('main1', ['glob' => 123])); Assert::same(' 10 - -', $latte->renderToString('main2', ['glob' => 123])); Assert::same(' 10 - -', $latte->renderToString('main3', ['glob' => 123])); Assert::same(' 10 5 -', $latte->renderToString('main4', ['glob' => 123])); Assert::same(' - - 123', $latte->renderToString('main5', ['glob' => 123])); +Assert::same(' - - 123', $latte->renderToString('main6', ['glob' => new \Exception("123")])); +Assert::same(' 10 5 -', $latte->renderToString('main7', ['glob' => 123])); +Assert::same(' 10 5 -', $latte->renderToString('main8', ['glob' => 123])); + +Assert::contains('/** @var int $a */', $latte->compile('inc3.latte')); +Assert::contains('/** @var ?\Exception $glob */', $latte->compile('inc6.latte')); diff --git a/tests/Latte/CoreMacros.templateType.80.phpt b/tests/Latte/CoreMacros.templateType.80.phpt new file mode 100644 index 000000000..3bc8d98e0 --- /dev/null +++ b/tests/Latte/CoreMacros.templateType.80.phpt @@ -0,0 +1,29 @@ +setLoader(new Latte\Loaders\StringLoader); + +class ExampleTemplateType { + public $a; + public int $b; + public ExampleTemplateType|int|null $c; + private $private; +} + +Assert::matchFile( + __DIR__ . '/expected/CoreMacros.templateType.80.phtml', + $latte->compile('{templateType ExampleTemplateType}{define test}{$a}{/define}') +); diff --git a/tests/Latte/CoreMacros.templateType.phpt b/tests/Latte/CoreMacros.templateType.phpt index 7ac8bcfa0..7bbf38a94 100644 --- a/tests/Latte/CoreMacros.templateType.phpt +++ b/tests/Latte/CoreMacros.templateType.phpt @@ -26,3 +26,15 @@ Assert::exception(function () use ($latte) { Assert::noError(function () use ($latte) { $latte->compile('{templateType stdClass}'); }); + +class ExampleTemplateType { + public $a; + public int $b; + public ?ExampleTemplateType $c; + private $private; +} + +Assert::matchFile( + __DIR__ . '/expected/CoreMacros.templateType.phtml', + $latte->compile('{templateType ExampleTemplateType}{define test}{$a}{/define}') +); diff --git a/tests/Latte/CoreMacros.varType.phpt b/tests/Latte/CoreMacros.varType.phpt index d9b0614e9..e2596bd8f 100644 --- a/tests/Latte/CoreMacros.varType.phpt +++ b/tests/Latte/CoreMacros.varType.phpt @@ -46,3 +46,28 @@ Assert::noError(function () use ($latte) { Assert::noError(function () use ($latte) { $latte->compile('{varType array{0: int, 1: int} $var}'); }); + +Assert::contains('/** @var int|null $var */', $latte->compile('{varType int|null $var}')); + +$template = <<<'XX' + +{varType string $a} + +{$a} + +{varType string $c} + +{include test} + +{define test} + {varType int $b} + {var $b = 5} + {$a}{$b} +{/define} + +XX; + +Assert::matchFile( + __DIR__ . '/expected/CoreMacros.varType.phtml', + $latte->compile($template) +); \ No newline at end of file diff --git a/tests/Latte/expected/BlockMacros.define.args6.html b/tests/Latte/expected/BlockMacros.define.args6.html new file mode 100644 index 000000000..128d3f783 --- /dev/null +++ b/tests/Latte/expected/BlockMacros.define.args6.html @@ -0,0 +1,7 @@ +default values + + +a) Variables 1, 123, 10 + + +b) Variables 1, 123, 10 diff --git a/tests/Latte/expected/BlockMacros.define.args6.phtml b/tests/Latte/expected/BlockMacros.define.args6.phtml new file mode 100644 index 000000000..fb4fee684 --- /dev/null +++ b/tests/Latte/expected/BlockMacros.define.args6.phtml @@ -0,0 +1,49 @@ + 'blockTest'], + ]; + + + public function main(): array + { + extract($this->params); + echo 'default values + +'; + if ($this->getParentName()) { + return get_defined_vars(); + } + echo ' +a) '; + $this->renderBlock('test', [1] + [], 'html') /* line %d% */; + echo ' + +b) '; + $this->renderBlock('test', ['var1' => 1] + [], 'html') /* line %d% */; + return get_defined_vars(); + } + + + /** {define test $var1 = 0, array $var2 = [1, 2, 3], int $var3 = 10} on line %d% */ + public function blockTest(array $ʟ_args): void + { + extract($this->params); + $var1 = $ʟ_args[0] ?? $ʟ_args['var1'] ?? 0; + /** @var array $var2 */ + $var2 = $ʟ_args[1] ?? $ʟ_args['var2'] ?? [1, 2, 3]; + /** @var int $var3 */ + $var3 = $ʟ_args[2] ?? $ʟ_args['var3'] ?? 10; + unset($ʟ_args); + echo ' Variables '; + echo LR\Filters::escapeHtmlText($var1) /* line %d% */; + echo ', '; + echo LR\Filters::escapeHtmlText(($this->filters->implode)($var2)) /* line %d% */; + echo ', '; + echo LR\Filters::escapeHtmlText($var3) /* line %d% */; + echo "\n"; + } + +} diff --git a/tests/Latte/expected/CoreMacros.templateType.80.phtml b/tests/Latte/expected/CoreMacros.templateType.80.phtml new file mode 100644 index 000000000..e1542e761 --- /dev/null +++ b/tests/Latte/expected/CoreMacros.templateType.80.phtml @@ -0,0 +1,16 @@ +params); + /** @var mixed $a (ExampleTemplateType) */ + /** @var int $b (ExampleTemplateType) */ + /** @var ExampleTemplateType|int|null $c (ExampleTemplateType) */ +%A% + public function blockTest(array $ʟ_args): void + { + extract($this->params); + /** @var mixed $a (ExampleTemplateType) */ + /** @var int $b (ExampleTemplateType) */ + /** @var ExampleTemplateType|int|null $c (ExampleTemplateType) */ +%A% \ No newline at end of file diff --git a/tests/Latte/expected/CoreMacros.templateType.phtml b/tests/Latte/expected/CoreMacros.templateType.phtml new file mode 100644 index 000000000..784f97c7e --- /dev/null +++ b/tests/Latte/expected/CoreMacros.templateType.phtml @@ -0,0 +1,16 @@ +params); + /** @var mixed $a (ExampleTemplateType) */ + /** @var int $b (ExampleTemplateType) */ + /** @var ?ExampleTemplateType $c (ExampleTemplateType) */ +%A% + public function blockTest(array $ʟ_args): void + { + extract($this->params); + /** @var mixed $a (ExampleTemplateType) */ + /** @var int $b (ExampleTemplateType) */ + /** @var ?ExampleTemplateType $c (ExampleTemplateType) */ +%A% \ No newline at end of file diff --git a/tests/Latte/expected/CoreMacros.varType.phtml b/tests/Latte/expected/CoreMacros.varType.phtml new file mode 100644 index 000000000..b255ee825 --- /dev/null +++ b/tests/Latte/expected/CoreMacros.varType.phtml @@ -0,0 +1,15 @@ +params); + /** @var string $a */ +%A% + public function blockTest(array $ʟ_args): void + { + extract($this->params); + /** @var string $a */ +%A% + /** @var int $b */ + $b = 5%a%; +%A% \ No newline at end of file