Skip to content

Commit

Permalink
Improves compilation of custom functions declared within a component'…
Browse files Browse the repository at this point in the history
…s template
  • Loading branch information
JohnathonKoster committed Feb 8, 2025
1 parent 8d94b4a commit b5fb64a
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- Improves compilation of custom functions declared within a component's template

## [v1.0.6](https://github.com/Stillat/dagger/compare/v1.0.5...v1.0.6) - 2025-01-31

- Corrects an issue where Blade stack compilation results in array index errors
Expand Down
2 changes: 2 additions & 0 deletions src/Compiler/ComponentCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Stillat\Dagger\Compiler\ComponentStages\ExtractsRenderCalls;
use Stillat\Dagger\Compiler\ComponentStages\RemoveUseStatements;
use Stillat\Dagger\Compiler\ComponentStages\ResolveNamespaces;
use Stillat\Dagger\Compiler\ComponentStages\RewriteFunctions;
use Stillat\Dagger\Compiler\Concerns\CompilesPhp;
use Stillat\Dagger\Parser\PhpParser;

Expand Down Expand Up @@ -42,6 +43,7 @@ public function compile(string $component): string
ResolveNamespaces::class,
RemoveUseStatements::class,
new ExtractsRenderCalls($this),
RewriteFunctions::class,
])
->thenReturn();

Expand Down
27 changes: 27 additions & 0 deletions src/Compiler/ComponentStages/RewriteFunctions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Stillat\Dagger\Compiler\ComponentStages;

use Closure;
use PhpParser\NodeTraverser;
use Stillat\Dagger\Parser\Visitors\FindFunctionsVisitor;
use Stillat\Dagger\Parser\Visitors\RenameFunctionVisitor;

class RewriteFunctions extends AbstractStage
{
public function handle($ast, Closure $next)
{
$traverser = new NodeTraverser;

$finder = new FindFunctionsVisitor;
$traverser->addVisitor($finder);
$traverser->traverse($ast);

$traverser->removeVisitor($finder);

$modifyFunctionsVisitor = new RenameFunctionVisitor($finder->getFunctionNames());
$traverser->addVisitor($modifyFunctionsVisitor);

return $next($traverser->traverse($ast));
}
}
25 changes: 25 additions & 0 deletions src/Parser/Visitors/FindFunctionsVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Stillat\Dagger\Parser\Visitors;

use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;

class FindFunctionsVisitor extends NodeVisitorAbstract
{
protected array $functionNames = [];

public function enterNode(Node $node)
{
if (! $node instanceof Node\Stmt\Function_) {
return;
}

$this->functionNames[] = $node->name->toString();
}

public function getFunctionNames(): array
{
return $this->functionNames;
}
}
70 changes: 70 additions & 0 deletions src/Parser/Visitors/RenameFunctionVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace Stillat\Dagger\Parser\Visitors;

use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
use Stillat\Dagger\Support\Utils;

class RenameFunctionVisitor extends NodeVisitorAbstract
{
protected array $functionNames = [];

public function __construct(array $functionNames)
{
$this->functionNames = $this->buildFunctionNameMap($functionNames);
}

protected function buildFunctionNameMap(array $functionNames): array
{
return collect($functionNames)
->mapWithKeys(function ($name) {
return [$name => $name.'_'.Utils::makeRandomString()];
})
->all();
}

public function enterNode(Node $node)
{
if (! $node instanceof Node\Expr\FuncCall || ! $node->name instanceof Node\Name) {
return;
}

$functionName = $node->name->toString();

if (! isset($this->functionNames[$functionName])) {
return;
}

$node->name = new Node\Name($this->functionNames[$functionName]);
}

public function leaveNode(Node $node)
{
if (! $node instanceof Node\Stmt\Function_) {
return null;
}

$functionName = $node->name->toString();

if (! isset($this->functionNames[$functionName])) {
return null;
}

$newFunctionName = $this->functionNames[$functionName];

$node->name = new Node\Identifier($newFunctionName);

return new Node\Stmt\If_(
new Node\Expr\BooleanNot(
new Node\Expr\FuncCall(
new Node\Name('function_exists'),
[new Node\Arg(new Node\Scalar\String_($newFunctionName))]
)
),
[
'stmts' => [$node],
]
);
}
}
22 changes: 22 additions & 0 deletions tests/Compiler/CustomFunctionsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

use Stillat\Dagger\Tests\CompilerTestCase;

uses(CompilerTestCase::class);

test('custom functions in a component are compiled', function () {
$template = <<<'BLADE'
@for ($i = 0; $i < 5; $i++)
<c-functions.declared title="The Title: {{ $i }}" />
@endfor
BLADE;

$expected = <<<'EXPECTED'
THE TITLE: 0THE TITLE: 1THE TITLE: 2THE TITLE: 3THE TITLE: 4
EXPECTED;

$this->assertSame(
$expected,
$this->render($template)
);
});
9 changes: 9 additions & 0 deletions tests/resources/components/functions/declared.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php
\Stillat\Dagger\component()->props(['title'])->trimOutput();
function toUpper($value) {
return mb_strtoupper($value);
}
?>

{{ toUpper($title) }}

0 comments on commit b5fb64a

Please sign in to comment.