templateDir = $templateDir; $this->devMode = $devMode; $this->cacheDir = $cacheDir ?? sys_get_temp_dir() . '/plate_cache'; if (!is_dir($this->cacheDir)) { @mkdir($this->cacheDir, 0755, true); } foreach ($globals as $key => $value) { if (!is_string($key)) { throw new InvalidArgumentException('Global key must be a string.'); } } $globals['_plate'] = $this; $this->globals = $globals; $this->renderer = new PhpRenderer($this->cacheDir, $this->globals); } /** * @throws ErrorException */ public function render(string $filename, array $context = []): string { if (pathinfo($filename, PATHINFO_EXTENSION) === 'php') { $render = new PhpRenderer($this->templateDir, $this->globals); return $render->render($filename, $context); } $templatePath = $this->templateDir . '/' . ltrim($filename, '/'); if (!file_exists($templatePath)) { throw new RuntimeException("Template not found: {$templatePath}"); } $cacheFile = $this->getCacheFilename($filename); if ($this->devMode === true || !$this->isCacheValid($templatePath, $cacheFile)) { $compiledCode = $this->compile($templatePath); try { token_get_all($compiledCode, TOKEN_PARSE); $this->saveToCache($cacheFile, trim($compiledCode) . PHP_EOL); } catch (ParseError $e) { $this->handleError($e, $compiledCode, $templatePath); } } try { set_error_handler(function ($severity, $message, $file, $line) { if (!(error_reporting() & $severity)) { return; } throw new ErrorException($message, 0, $severity, $file, $line); }); return $this->renderer->render(str_replace($this->cacheDir, '', realpath($cacheFile)), $context); } catch (Throwable $e) { $this->handleError($e, file_get_contents($cacheFile), $templatePath); } finally { restore_error_handler(); } } /** * @throws ErrorException */ public function extend(string $filename): void { $_ = $this->render($filename); $cacheFile = $this->getCacheFilename($filename); $this->renderer->extend(str_replace($this->cacheDir, '', realpath($cacheFile))); } private function compile(string $path): string { $html = file_get_contents($path); $this->blocks = []; $lines = explode(PHP_EOL, $html); $output = ""; foreach ($lines as $i => $line) { $num = $i + 1; $line = preg_replace('/{#.*?#}/', '', $line); if (trim($line) === '') { $output .= $line .PHP_EOL; continue; } $line = preg_replace_callback('/{{(.*?)}}|{% (.*?) %}/', function ($m) use ($num, $path) { $isBlock = !empty($m[2]); $content = trim($isBlock ? $m[2] : $m[1]); if ($isBlock) { return $this->compileStructure($content, $num); } $phpExpr = $this->parseTokens($content); return ""; }, $line); $output .= $line .PHP_EOL; } return $output; } private function compileStructure(string $rawContent, int $line): string { $parts = preg_split('/\s+/', $rawContent, 2); $cmd = strtolower(trim($parts[0])); $rawExpr = $parts[1] ?? ''; if ($cmd === 'extends') { $phpExpr = $this->parseTokens($rawExpr); return "extend($phpExpr); ?>"; } if ($cmd === 'block') { $blockName = trim($rawExpr, "\"' "); return "startBlock('$blockName'); ?>"; } if ($cmd === 'endblock') { return "endblock(); ?>"; } if ($cmd === 'include') { $phpExpr = $this->parseTokens($rawExpr); return "render($phpExpr); ?>"; } if ($cmd === 'set') { $phpExpr = $this->parseTokens($rawExpr); return ""; } if (in_array($cmd, ['if', 'foreach', 'while', 'for', 'elseif'])) { if ($cmd !== 'elseif') { $this->blocks[] = ['type' => $cmd, 'line' => $line]; } $phpExpr = $this->parseTokens($rawExpr); return ""; } if ($cmd === 'else') { return ""; } if (substr($cmd, 0, 3) === 'end') { if (empty($this->blocks)) { throw new \Exception(sprintf( "Unexpected '%s' at line %d (missing opening tag)", $cmd, $line )); } array_pop($this->blocks); return ""; } $phpExpr = $this->parseTokens($rawContent); return ""; } private function parseTokens(string $expr): string { if (trim($expr) === '') return ''; $tokens = token_get_all("'); $isReserved = in_array($word, ['as', 'is', 'true', 'false', 'null', 'empty']); $res .= ($isProp || $isReserved || $isFunction) ? $text : '$' . $text; } else { $res .= $text; } } else { $res .= ($t === '.') ? '->' : $t; } } return $res; } private function handleError(\Throwable $e, string $compiled, string $path): void { $lines = explode("\n", $compiled); $errorLine = $e->getLine(); $faultyCode = $lines[$errorLine - 1] ?? ''; preg_match('/\/\*L:(\d+);F:(.*?)\*\//', $faultyCode, $m); $currentFile = $m[2] ?? $path; $currentLine = $m[1] ?? 'unknown'; if ($e instanceof \ErrorException) { throw new \ErrorException( $e->getMessage() . " -> " . $currentFile . ":" . $currentLine, $e->getCode(), $e->getSeverity(), $e->getFile(), $e->getLine(), $e->getPrevious() ); } throw new \ErrorException( "PurePlate Error: " . $e->getMessage() . " [At: " . $currentFile . ":" . $currentLine . "]", $e->getCode(), E_USER_ERROR, $e->getFile(), $e->getLine(), $e ); } private function isCacheValid(string $templateFile, string $cacheFile): bool { if (!file_exists($cacheFile)) { return false; } return filemtime($cacheFile) >= filemtime($templateFile); } private function getCacheFilename(string $templateFile): string { $hash = md5(realpath($templateFile)); $basename = pathinfo($templateFile, PATHINFO_FILENAME); return $this->cacheDir . DIRECTORY_SEPARATOR . $basename . '_' . $hash . '.cache.php'; } private function saveToCache(string $cacheFile, string $compiledCode): void { $tempFile = $cacheFile . '.tmp'; if (file_put_contents($tempFile, $compiledCode, LOCK_EX) !== false) { @rename($tempFile, $cacheFile); } else { throw new RuntimeException("Unable to write cache file: {$cacheFile}"); } } }