PromptVault/app/Services/PromptRenderer.php
2026-10-26 15:12:16 -05:00

69 lines
1.9 KiB
PHP

<?php
namespace App\Services;
use App\Exceptions\PromptRenderException;
use RuntimeException;
/**
* Pure prompt template renderer. No framework dependencies.
*
* Variable syntax: `{{ name }}` where name matches `[A-Za-z0-9_-]+`.
* Rendering is strict: it throws PromptRenderException when any variable
* lacks a string value. Values are inserted verbatim, in a single pass
* (inserted values are never re-scanned for variables).
*/
class PromptRenderer
{
public const VARIABLE_PATTERN = '/\{\{\s*([A-Za-z0-9_-]+)\s*\}\}/';
/**
* Extract the unique variable names in first-occurrence order.
*
* @return list<string>
*/
public function extractVariables(string $body): array
{
if (preg_match_all(self::VARIABLE_PATTERN, $body, $matches) === false) {
return [];
}
return array_values(array_unique($matches[1]));
}
/**
* Render the template with the given values.
*
* @param array<string, mixed> $values
*
* @throws PromptRenderException when any variable has no string value
* @throws RuntimeException when the template cannot be processed
*/
public function render(string $body, array $values): string
{
$missing = [];
foreach ($this->extractVariables($body) as $variable) {
if (! array_key_exists($variable, $values) || ! is_string($values[$variable])) {
$missing[] = $variable;
}
}
if ($missing !== []) {
throw PromptRenderException::missing($missing);
}
$rendered = preg_replace_callback(
self::VARIABLE_PATTERN,
static fn (array $matches): string => (string) ($values[$matches[1]] ?? $matches[0]),
$body
);
if ($rendered === null) {
throw new RuntimeException('Prompt template could not be rendered.');
}
return $rendered;
}
}