From 3cc02af7d460706f03d1abd1679c6c3ddf9cdf96 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 00:10:12 +0000 Subject: [PATCH 1/2] Initial plan From da0b464dce9790a54b6b6babeddf9e569ad0403d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 00:15:09 +0000 Subject: [PATCH 2/2] fix: compile template expressions with valid PHP variables --- README.md | 2 +- autoload.php | 27 ++- examples/index.php | 2 +- src/Core/View/Abstract/AbstractDirective.php | 2 +- src/Core/View/Abstract/AbstractFilter.php | 2 +- src/Core/View/Abstract/AbstractMiddleware.php | 2 +- src/Core/View/Abstract/AbstractNode.php | 6 +- src/Core/View/Cache/CacheInterface.php | 2 +- src/Core/View/Cache/CacheKey.php | 2 +- src/Core/View/Cache/CacheManager.php | 2 +- src/Core/View/Cache/FileCacheAdapter.php | 2 +- src/Core/View/Cache/FileWatcher.php | 2 +- src/Core/View/Compiler.php | 176 +++++++++++++++--- .../View/Components/ComponentRegistry.php | 4 +- src/Core/View/Debug/ErrorFormatter.php | 2 +- src/Core/View/Debug/RuntimeContext.php | 2 +- src/Core/View/Debug/SourceMap.php | 2 +- src/Core/View/Debug/TemplateDebugger.php | 2 +- src/Core/View/Directives/BlockDirective.php | 4 +- .../View/Directives/DirectiveRegistry.php | 4 +- src/Core/View/Directives/ForeachDirective.php | 4 +- src/Core/View/Directives/IfDirective.php | 4 +- src/Core/View/Directives/IncludeDirective.php | 4 +- src/Core/View/Directives/SetDirective.php | 4 +- src/Core/View/Engine.php | 38 ++-- src/Core/View/Environment.php | 2 +- src/Core/View/Escape/Escaper.php | 2 +- src/Core/View/Escape/HtmlEscaper.php | 2 +- src/Core/View/Escape/JavaScriptEscaper.php | 2 +- src/Core/View/Escape/UriEscaper.php | 2 +- .../Exceptions/CircularReferenceException.php | 2 +- src/Core/View/Exceptions/CompileException.php | 2 +- .../View/Exceptions/CompilerException.php | 2 +- src/Core/View/Exceptions/ParserException.php | 2 +- src/Core/View/Exceptions/RuntimeException.php | 2 +- src/Core/View/Exceptions/SyntaxException.php | 2 +- src/Core/View/Exceptions/ViewException.php | 2 +- src/Core/View/Filters/ArrayFilters.php | 2 +- src/Core/View/Filters/DateFilters.php | 2 +- src/Core/View/Filters/FilterRegistry.php | 4 +- src/Core/View/Filters/NumberFilters.php | 2 +- src/Core/View/Filters/StringFilters.php | 2 +- src/Core/View/Layout/BlockStack.php | 2 +- src/Core/View/Layout/LayoutManager.php | 4 +- src/Core/View/Layout/LayoutResolver.php | 2 +- src/Core/View/Lexer.php | 4 +- src/Core/View/Middleware/CacheMiddleware.php | 4 +- .../View/Middleware/MiddlewarePipeline.php | 4 +- .../View/Middleware/ProfilingMiddleware.php | 4 +- .../View/Middleware/SecurityMiddleware.php | 4 +- src/Core/View/Nodes/BlockNode.php | 6 +- src/Core/View/Nodes/ComponentNode.php | 6 +- src/Core/View/Nodes/ExpressionNode.php | 6 +- src/Core/View/Nodes/ForeachNode.php | 6 +- src/Core/View/Nodes/IfNode.php | 6 +- src/Core/View/Nodes/IncludeNode.php | 6 +- src/Core/View/Nodes/NodeInterface.php | 4 +- src/Core/View/Nodes/RawNode.php | 6 +- src/Core/View/Nodes/SetNode.php | 6 +- src/Core/View/Nodes/TextNode.php | 6 +- src/Core/View/Parser.php | 38 ++-- src/Core/View/Renderer.php | 6 +- src/Core/View/Scope/Scope.php | 2 +- src/Core/View/Scope/ScopeStack.php | 2 +- src/Core/View/Scope/Variable.php | 2 +- src/Core/View/TemplateResolver.php | 4 +- src/Core/View/Validation/SyntaxValidator.php | 4 +- .../View/Validation/TemplateValidator.php | 2 +- 68 files changed, 316 insertions(+), 169 deletions(-) diff --git a/README.md b/README.md index 3135a2e..8ebcd3e 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ __DIR__ . '/templates', diff --git a/autoload.php b/autoload.php index 7f0c689..9890e94 100644 --- a/autoload.php +++ b/autoload.php @@ -6,17 +6,30 @@ */ spl_autoload_register(function ($class) { - $prefix = 'Beobles\\Core\\View\\'; + $prefixes = ['Core\\View\\', 'Beobles\\Core\\View\\']; $baseDir = __DIR__ . '/src/Core/View/'; - if (strpos($class, $prefix) !== 0) { - return; - } + foreach ($prefixes as $prefix) { + if (strpos($class, $prefix) !== 0) { + continue; + } + + $relativeClass = substr($class, strlen($prefix)); + $file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php'; - $relativeClass = substr($class, strlen($prefix)); - $file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php'; + if (!file_exists($file)) { + return; + } - if (file_exists($file)) { require $file; + + if ($prefix === 'Beobles\\Core\\View\\') { + $newClass = 'Core\\View\\' . $relativeClass; + if (class_exists($newClass, false) && !class_exists($class, false)) { + class_alias($newClass, $class); + } + } + + return; } }); diff --git a/examples/index.php b/examples/index.php index 39253a0..9392a5e 100644 --- a/examples/index.php +++ b/examples/index.php @@ -2,7 +2,7 @@ require_once __DIR__ . '/../autoload.php'; -use Beobles\Core\View\Engine; +use Core\View\Engine; // Criar engine $engine = new Engine([ diff --git a/src/Core/View/Abstract/AbstractDirective.php b/src/Core/View/Abstract/AbstractDirective.php index 6bde27f..529c943 100644 --- a/src/Core/View/Abstract/AbstractDirective.php +++ b/src/Core/View/Abstract/AbstractDirective.php @@ -1,6 +1,6 @@ resolveValue(' . var_export($base, true) . ', get_defined_vars())'; - } else { - $compiled = $this->transformDotNotation($base); - } + $compiled = $this->transformExpression($base); foreach ($parts as $part) { $part = trim($part); @@ -164,15 +159,154 @@ private function compileExpression(string $expression): string return $compiled; } - private function transformDotNotation(string $expression): string + private function transformExpression(string $expression): string + { + $expression = trim($expression); + if ($expression === '') { + return 'null'; + } + + $expression = $this->transformMemberNotation($expression); + return $this->prefixBareIdentifiers($expression); + } + + private function transformMemberNotation(string $expression): string { + $expression = preg_replace( + '/\b([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)\s*\(/', + '\$$1->$2(', + $expression + ) ?? $expression; + return preg_replace_callback( - '/\b([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)+)\b/', - fn(array $matches): string => '$__engine->resolveValue(' . var_export($matches[1], true) . ', get_defined_vars())', + '/\b(\$?[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)+)\b/', + static function (array $matches): string { + $path = $matches[1]; + $segments = explode('.', ltrim($path, '$')); + $result = '$' . array_shift($segments); + + foreach ($segments as $segment) { + $result .= "['{$segment}']"; + } + + return $result; + }, $expression ) ?? $expression; } + private function prefixBareIdentifiers(string $expression): string + { + $tokens = token_get_all('shouldPrefixIdentifier($text, $tokens, $i)) { + $compiled .= $text; + continue; + } + + $compiled .= '$' . $text; + } + + return trim($compiled); + } + + /** @param array $tokens */ + private function shouldPrefixIdentifier(string $text, array $tokens, int $index): bool + { + $lower = strtolower($text); + if (in_array($lower, [ + 'true', + 'false', + 'null', + 'and', + 'or', + 'xor', + 'instanceof', + 'new', + 'clone', + 'default', + 'match', + 'fn', + 'self', + 'static', + 'parent', + ], true)) { + return false; + } + + if (preg_match('/^[A-Z_][A-Z0-9_]*$/', $text) === 1) { + return false; + } + + $prev = $this->previousSignificantToken($tokens, $index); + $next = $this->nextSignificantToken($tokens, $index); + + if (($prev['text'] ?? null) === '->' || ($prev['text'] ?? null) === '::' || ($prev['id'] ?? null) === T_VARIABLE) { + return false; + } + + if (($next['text'] ?? null) === '(') { + return false; + } + + return true; + } + + /** @param array $tokens + * @return array{id:int|null,text:string|null} + */ + private function previousSignificantToken(array $tokens, int $index): array + { + for ($i = $index - 1; $i >= 0; $i--) { + $token = $tokens[$i]; + if (is_string($token)) { + if (trim($token) !== '') { + return ['id' => null, 'text' => $token]; + } + continue; + } + + if ($token[0] !== T_WHITESPACE) { + return ['id' => $token[0], 'text' => $token[1]]; + } + } + + return ['id' => null, 'text' => null]; + } + + /** @param array $tokens + * @return array{id:int|null,text:string|null} + */ + private function nextSignificantToken(array $tokens, int $index): array + { + $count = count($tokens); + for ($i = $index + 1; $i < $count; $i++) { + $token = $tokens[$i]; + if (is_string($token)) { + if (trim($token) !== '') { + return ['id' => null, 'text' => $token]; + } + continue; + } + + if ($token[0] !== T_WHITESPACE) { + return ['id' => $token[0], 'text' => $token[1]]; + } + } + + return ['id' => null, 'text' => null]; + } + /** @param array $nodes */ private function compileChildren(array $nodes): string { diff --git a/src/Core/View/Components/ComponentRegistry.php b/src/Core/View/Components/ComponentRegistry.php index f7a1e70..30b8135 100644 --- a/src/Core/View/Components/ComponentRegistry.php +++ b/src/Core/View/Components/ComponentRegistry.php @@ -1,8 +1,8 @@