Files
michaelschiemer/src/Framework/Notification/Templates/TemplateRenderer.php
Michael Schiemer 3b623e7afb feat(Deployment): Integrate Ansible deployment via PHP deployment pipeline
- Create AnsibleDeployStage using framework's Process module for secure command execution
- Integrate AnsibleDeployStage into DeploymentPipelineCommands for production deployments
- Add force_deploy flag support in Ansible playbook to override stale locks
- Use PHP deployment module as orchestrator (php console.php deploy:production)
- Fix ErrorAggregationInitializer to use Environment class instead of $_ENV superglobal

Architecture:
- BuildStage → AnsibleDeployStage → HealthCheckStage for production
- Process module provides timeout, error handling, and output capture
- Ansible playbook supports rollback via rollback-git-based.yml
- Zero-downtime deployments with health checks
2025-10-26 14:08:07 +01:00

197 lines
6.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Notification\Templates;
use App\Framework\Notification\Notification;
use App\Framework\Notification\ValueObjects\NotificationChannel;
use App\Framework\Notification\ValueObjects\NotificationTypeInterface;
/**
* Template Renderer
*
* Renders notification templates with variable substitution
* Supports per-channel customization and placeholder replacement
*/
final readonly class TemplateRenderer
{
/**
* Render a notification from a template
*
* @param NotificationTemplate $template The template to render
* @param string $recipientId Recipient identifier
* @param array<string, mixed> $variables Variables for placeholder substitution
* @param array<NotificationChannel> $channels Target channels
* @param NotificationTypeInterface $type Notification type
* @return Notification Rendered notification
*/
public function render(
NotificationTemplate $template,
string $recipientId,
array $variables,
array $channels,
NotificationTypeInterface $type
): Notification {
// Validate required variables
$template->validateVariables($variables);
// Merge with default variables
$mergedVariables = [...$template->defaultVariables, ...$variables];
// Render title and body
$title = $this->replacePlaceholders($template->titleTemplate, $mergedVariables);
$body = $this->replacePlaceholders($template->bodyTemplate, $mergedVariables);
// Create base notification
$notification = Notification::create(
recipientId: $recipientId,
type: $type,
title: $title,
body: $body,
...$channels
)->withPriority($template->defaultPriority);
// Store template information in data
return $notification->withData([
'template_id' => $template->id->toString(),
'template_name' => $template->name,
'template_variables' => $mergedVariables,
]);
}
/**
* Render for a specific channel with channel-specific template
*
* @param NotificationTemplate $template The template
* @param NotificationChannel $channel Target channel
* @param array<string, mixed> $variables Variables for substitution
* @return RenderedContent Rendered title and body for the channel
*/
public function renderForChannel(
NotificationTemplate $template,
NotificationChannel $channel,
array $variables
): RenderedContent {
// Validate required variables
$template->validateVariables($variables);
// Merge with default variables
$mergedVariables = [...$template->defaultVariables, ...$variables];
// Get channel-specific template if available
$channelTemplate = $template->getChannelTemplate($channel);
// Determine which templates to use
$titleTemplate = $channelTemplate?->titleTemplate ?? $template->titleTemplate;
$bodyTemplate = $channelTemplate?->bodyTemplate ?? $template->bodyTemplate;
// Render
$title = $this->replacePlaceholders($titleTemplate, $mergedVariables);
$body = $this->replacePlaceholders($bodyTemplate, $mergedVariables);
// Get channel metadata
$metadata = $channelTemplate?->metadata ?? [];
return new RenderedContent(
title: $title,
body: $body,
metadata: $metadata
);
}
/**
* Replace placeholders in template string
*
* Supports {{variable}} and {{variable.nested}} syntax
*
* @param string $template Template string with placeholders
* @param array<string, mixed> $variables Variable values
* @return string Rendered string
*/
private function replacePlaceholders(string $template, array $variables): string
{
return preg_replace_callback(
'/\{\{([a-zA-Z0-9_.]+)\}\}/',
function ($matches) use ($variables) {
$key = $matches[1];
// Support nested variables like {{user.name}}
if (str_contains($key, '.')) {
$value = $this->getNestedValue($variables, $key);
} else {
$value = $variables[$key] ?? '';
}
// Convert to string
return $this->valueToString($value);
},
$template
);
}
/**
* Get nested value from array using dot notation
*
* @param array<string, mixed> $array Source array
* @param string $key Dot-notated key (e.g., 'user.name')
* @return mixed Value or empty string if not found
*/
private function getNestedValue(array $array, string $key): mixed
{
$keys = explode('.', $key);
$value = $array;
foreach ($keys as $segment) {
if (is_array($value) && array_key_exists($segment, $value)) {
$value = $value[$segment];
} else {
return '';
}
}
return $value;
}
/**
* Convert value to string for template substitution
*
* @param mixed $value Value to convert
* @return string String representation
*/
private function valueToString(mixed $value): string
{
if ($value === null) {
return '';
}
if (is_bool($value)) {
return $value ? 'true' : 'false';
}
if (is_scalar($value)) {
return (string) $value;
}
if (is_array($value)) {
return json_encode($value);
}
if (is_object($value)) {
// Handle objects with __toString
if (method_exists($value, '__toString')) {
return (string) $value;
}
// Handle objects with toArray
if (method_exists($value, 'toArray')) {
return json_encode($value->toArray());
}
return json_encode($value);
}
return '';
}
}