From 50f8883f01e97f91163193b1a8bf0b50663976f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 00:23:27 +0000 Subject: [PATCH 1/3] Initial plan From 5ae307f9f6a48bdb0bb7194d34466aca24d427c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 00:28:13 +0000 Subject: [PATCH 2/3] feat: replace eval renderer with class-based compiled template cache --- src/Core/View/Cache/CompiledTemplateCache.php | 129 ++++++++++++++++ src/Core/View/CompiledTemplate.php | 9 ++ src/Core/View/Compiler.php | 145 ++++++++++++++++-- src/Core/View/Engine.php | 4 +- src/Core/View/Renderer.php | 67 +++----- 5 files changed, 300 insertions(+), 54 deletions(-) create mode 100644 src/Core/View/Cache/CompiledTemplateCache.php create mode 100644 src/Core/View/CompiledTemplate.php diff --git a/src/Core/View/Cache/CompiledTemplateCache.php b/src/Core/View/Cache/CompiledTemplateCache.php new file mode 100644 index 0000000..60b66c1 --- /dev/null +++ b/src/Core/View/Cache/CompiledTemplateCache.php @@ -0,0 +1,129 @@ +cacheDir)) { + mkdir($this->cacheDir, 0755, true); + } + } + + /** @param array $dependencies */ + public function load(string $templatePath, string $compiledCode, array $dependencies = []): string + { + $cacheKey = hash('sha256', $templatePath); + $cacheFile = $this->getCacheFile($cacheKey); + $metaFile = $this->getMetaFile($cacheFile); + + if ($this->isCacheValid($cacheFile, $metaFile, $dependencies)) { + return $this->requireCompiledClass($cacheFile, $metaFile); + } + + return $this->writeCache($cacheFile, $metaFile, $compiledCode, $dependencies); + } + + private function getCacheFile(string $key): string + { + return rtrim($this->cacheDir, '/\\') . '/' . substr($key, 0, 2) . '/' . substr($key, 2) . '.php'; + } + + private function getMetaFile(string $cacheFile): string + { + return substr($cacheFile, 0, -4) . '.meta'; + } + + /** @param array $dependencies */ + private function isCacheValid(string $cacheFile, string $metaFile, array $dependencies): bool + { + if (!is_file($cacheFile) || !is_file($metaFile)) { + return false; + } + + $meta = $this->readMeta($metaFile); + if (!is_array($meta)) { + return false; + } + + $expectedHash = hash_file('sha256', $cacheFile); + if (($meta['hash'] ?? null) !== $expectedHash) { + return false; + } + + $cacheTime = filemtime($cacheFile) ?: 0; + foreach ($dependencies as $dependency) { + if (!is_file($dependency) || ((filemtime($dependency) ?: 0) > $cacheTime)) { + return false; + } + } + + return true; + } + + /** @param array $dependencies */ + private function writeCache(string $cacheFile, string $metaFile, string $compiledCode, array $dependencies): string + { + $dir = dirname($cacheFile); + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + } + + file_put_contents($cacheFile, $compiledCode); + chmod($cacheFile, 0644); + + $class = $this->extractClassName($compiledCode); + $meta = [ + 'generated_at' => time(), + 'dependencies' => $dependencies, + 'hash' => hash_file('sha256', $cacheFile), + 'class' => $class, + ]; + + file_put_contents($metaFile, json_encode($meta, JSON_PRETTY_PRINT)); + + return $this->requireCompiledClass($cacheFile, $metaFile); + } + + private function requireCompiledClass(string $cacheFile, string $metaFile): string + { + $meta = $this->readMeta($metaFile); + if (is_array($meta) && isset($meta['class']) && is_string($meta['class']) && $meta['class'] !== '' && class_exists($meta['class'])) { + return $meta['class']; + } + + return (string) require $cacheFile; + } + + private function readMeta(string $metaFile): ?array + { + $content = file_get_contents($metaFile); + if ($content === false) { + return null; + } + + $decoded = json_decode($content, true); + return is_array($decoded) ? $decoded : null; + } + + private function extractClassName(string $compiledCode): string + { + $namespace = ''; + if (preg_match('/namespace\s+([^;]+);/', $compiledCode, $namespaceMatch) === 1) { + $namespace = trim($namespaceMatch[1]); + } + + $class = ''; + if (preg_match('/class\s+([A-Za-z_][A-Za-z0-9_]*)/', $compiledCode, $classMatch) === 1) { + $class = $classMatch[1]; + } + + if ($class === '') { + return ''; + } + + return $namespace === '' ? $class : $namespace . '\\' . $class; + } +} + diff --git a/src/Core/View/CompiledTemplate.php b/src/Core/View/CompiledTemplate.php new file mode 100644 index 0000000..f146a00 --- /dev/null +++ b/src/Core/View/CompiledTemplate.php @@ -0,0 +1,9 @@ + $nodes */ - public function compile(array $nodes): string + public function compile(array $nodes, string $sourceFile = ''): string { - $code = "sourceFile = $sourceFile; + $this->className = $this->generateClassName($sourceFile); - foreach ($nodes as $node) { - $code .= $this->compileNode($node); - } - - return $code; + return $this->generateHeader() + . $this->generateClassStart() + . $this->compileChildren($nodes) + . $this->generateClassEnd(); } public function compileNode(NodeInterface $node): string @@ -142,7 +144,7 @@ private function compileExpression(string $expression): string if (preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $base) === 1) { $compiled = '$__engine->resolveValue(' . var_export($base, true) . ', get_defined_vars())'; } else { - $compiled = $this->transformDotNotation($base); + $compiled = $this->normalizeBareVariables($this->transformDotNotation($base)); } foreach ($parts as $part) { @@ -173,6 +175,91 @@ private function transformDotNotation(string $expression): string ) ?? $expression; } + private function normalizeBareVariables(string $expression): string + { + $tokens = token_get_all(' $token) { + if (!is_array($token) || $token[0] !== T_STRING) { + $result .= is_array($token) ? $token[1] : $token; + continue; + } + + $text = $token[1]; + $lower = strtolower($text); + if (in_array($lower, $skip, true)) { + $result .= $text; + continue; + } + + if (preg_match('/^[A-Z_][A-Z0-9_]*$/', $text) === 1) { + $result .= $text; + continue; + } + + $prev = $this->getPreviousSignificantToken($tokens, $index); + $next = $this->getNextSignificantToken($tokens, $index); + $prevText = is_array($prev) ? $prev[1] : $prev; + $nextText = is_array($next) ? $next[1] : $next; + $prevType = is_array($prev) ? $prev[0] : null; + + if ( + $prevText === '$' + || $prevText === '->' + || $prevText === '::' + || $prevText === '\\' + || $nextText === '(' + || $prevType === T_NEW + || $prevType === T_FUNCTION + || $prevType === T_FN + ) { + $result .= $text; + continue; + } + + $result .= '$' . $text; + } + + return $result; + } + + /** @param array $tokens */ + private function getPreviousSignificantToken(array $tokens, int $index): mixed + { + for ($i = $index - 1; $i >= 0; $i--) { + $token = $tokens[$i]; + if (is_array($token) && in_array($token[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) { + continue; + } + + return $token; + } + + return null; + } + + /** @param array $tokens */ + private function getNextSignificantToken(array $tokens, int $index): mixed + { + $count = count($tokens); + for ($i = $index + 1; $i < $count; $i++) { + $token = $tokens[$i]; + if (is_array($token) && in_array($token[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) { + continue; + } + + return $token; + } + + return null; + } + /** @param array $nodes */ private function compileChildren(array $nodes): string { @@ -202,4 +289,44 @@ private function indent(string $code, int $spaces = 8): string return implode("\n", array_map(static fn(string $line): string => $line === '' ? $line : $indent . $line, $lines)) . "\n"; } + + private function generateHeader(): string + { + $source = $this->sourceFile !== '' ? $this->sourceFile : '[inline-template]'; + + return "className} extends CompiledTemplate\n{\n" + . " public function render(array \$data = [], ?Engine \$engine = null): string\n" + . " {\n" + . " \$__engine = \$engine;\n" + . " \$__loop_stack = \$__loop_stack ?? [];\n" + . " extract(\$data, EXTR_SKIP);\n" + . " ob_start();\n\n"; + } + + private function generateClassEnd(): string + { + return "\n return (string) ob_get_clean();\n" + . " }\n" + . "}\n\n" + . 'return ' . $this->className . "::class;\n"; + } + + private function generateClassName(string $sourceFile): string + { + $seed = $sourceFile !== '' ? $sourceFile : uniqid('inline_', true); + return 'Template' . strtoupper(hash('crc32b', $seed)); + } } diff --git a/src/Core/View/Engine.php b/src/Core/View/Engine.php index c9a771e..b5b10fc 100644 --- a/src/Core/View/Engine.php +++ b/src/Core/View/Engine.php @@ -103,7 +103,7 @@ function (array $context) use ($templatePath, $data, $start): string { $this->scopeStack->set((string) $name, $value); } - return $this->renderer->render($compiledCode, $runtimeData, $this); + return $this->renderer->render($compiledCode, $runtimeData, $this, $absolutePath); } catch (\Throwable $e) { throw new ViewException("Error rendering template '{$templatePath}': " . $e->getMessage(), 0, $e); } @@ -198,7 +198,7 @@ private function compileTemplate(string $absolutePath, string $templatePath): st $tokens = $this->lexer->tokenize($merged, $absolutePath); $ast = $this->parser->parse($tokens); - $compiledCode = $this->compiler->compile($ast); + $compiledCode = $this->compiler->compile($ast, $absolutePath); if ($this->cacheEnabled) { $this->cacheManager->set($cacheKey, $compiledCode); diff --git a/src/Core/View/Renderer.php b/src/Core/View/Renderer.php index 8e08c1a..0071897 100644 --- a/src/Core/View/Renderer.php +++ b/src/Core/View/Renderer.php @@ -2,62 +2,43 @@ namespace Beobles\Core\View; +use Beobles\Core\View\Cache\CompiledTemplateCache; use Beobles\Core\View\Exceptions\RuntimeException; -use Beobles\Core\View\Exceptions\SyntaxException; class Renderer { + private CompiledTemplateCache $compiledTemplateCache; + public function __construct(private ?string $compiledTemplatesDir = null) { + $targetDir = $this->compiledTemplatesDir ?? sys_get_temp_dir(); + $this->compiledTemplateCache = new CompiledTemplateCache($targetDir); } - public function render(string $compiledCode, array $data = [], ?Engine $engine = null): string - { - $compiledFile = $this->writeCompiledFile($compiledCode, $engine); - $this->assertValidPhp($compiledFile); + public function render( + string $compiledCode, + array $data = [], + ?Engine $engine = null, + string $sourceFile = '' + ): string { + $templateId = $sourceFile !== '' ? $sourceFile : hash('sha256', $compiledCode); + $dependencies = $sourceFile !== '' ? [$sourceFile] : []; + $className = $this->compiledTemplateCache->load($templateId, $compiledCode, $dependencies); + + if (!class_exists($className)) { + throw new RuntimeException("Compiled template class not found: {$className}"); + } - $render = static function (string $__compiledFile, array $__data, ?Engine $__engine): string { - extract($__data, EXTR_SKIP); - ob_start(); - include $__compiledFile; - return (string) ob_get_clean(); - }; + $template = new $className(); + if (!$template instanceof CompiledTemplate) { + throw new RuntimeException('Compiled template must extend ' . CompiledTemplate::class); + } try { - return $render($compiledFile, $data, $engine); + return $template->render($data, $engine); } catch (\Throwable $e) { throw new RuntimeException('Runtime render error: ' . $e->getMessage(), 0, $e); } } - - private function writeCompiledFile(string $compiledCode, ?Engine $engine): string - { - $targetDir = $this->compiledTemplatesDir; - if ($targetDir === null && $engine !== null) { - $targetDir = $engine->getEnvironment()->getConfig('compiled_templates_dir', $engine->getEnvironment()->getConfig('cache_dir', sys_get_temp_dir())); - } - - $targetDir ??= sys_get_temp_dir(); - - if (!is_dir($targetDir)) { - mkdir($targetDir, 0755, true); - } - - $file = rtrim($targetDir, '/\\') . '/tpl_' . md5($compiledCode) . '.php'; - file_put_contents($file, $compiledCode); - - return $file; - } - - private function assertValidPhp(string $compiledFile): void - { - $phpBinary = PHP_BINARY ?: 'php'; - $output = []; - $status = 0; - @exec(escapeshellcmd($phpBinary) . ' -l ' . escapeshellarg($compiledFile) . ' 2>&1', $output, $status); - - if ($status !== 0) { - throw new SyntaxException('Compiled template syntax error: ' . implode("\n", $output)); - } - } } + From fc971fff78235cdf15530adafb19c153f6527fb1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 02:20:54 +0000 Subject: [PATCH 3/3] fix: remove Beobles prefix from view namespaces --- README.md | 2 +- autoload.php | 2 +- 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/CompiledTemplateCache.php | 2 +- src/Core/View/Cache/FileCacheAdapter.php | 2 +- src/Core/View/Cache/FileWatcher.php | 2 +- src/Core/View/CompiledTemplate.php | 2 +- src/Core/View/Compiler.php | 30 +++++++-------- .../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 +- 70 files changed, 159 insertions(+), 159 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..dc788e7 100644 --- a/autoload.php +++ b/autoload.php @@ -6,7 +6,7 @@ */ spl_autoload_register(function ($class) { - $prefix = 'Beobles\\Core\\View\\'; + $prefix = 'Core\\View\\'; $baseDir = __DIR__ . '/src/Core/View/'; if (strpos($class, $prefix) !== 0) { 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 @@ sourceFile !== '' ? $this->sourceFile : '[inline-template]'; return "