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
This commit is contained in:
196
src/Framework/Notification/Templates/TemplateRenderer.php
Normal file
196
src/Framework/Notification/Templates/TemplateRenderer.php
Normal file
@@ -0,0 +1,196 @@
|
||||
<?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 '';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user