- 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
272 lines
8.5 KiB
PHP
272 lines
8.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Notification;
|
|
|
|
use App\Framework\Core\ValueObjects\Timestamp;
|
|
use App\Framework\Notification\ValueObjects\NotificationChannel;
|
|
use App\Framework\Notification\ValueObjects\NotificationId;
|
|
use App\Framework\Notification\ValueObjects\NotificationPriority;
|
|
use App\Framework\Notification\ValueObjects\NotificationStatus;
|
|
use App\Framework\Notification\ValueObjects\NotificationTypeInterface;
|
|
|
|
/**
|
|
* Core notification entity
|
|
*
|
|
* Immutable value object representing a notification
|
|
*/
|
|
final readonly class Notification
|
|
{
|
|
/**
|
|
* @param NotificationId $id Unique notification identifier
|
|
* @param string $recipientId User/Entity receiving the notification
|
|
* @param NotificationTypeInterface $type Notification category
|
|
* @param string $title Notification title
|
|
* @param string $body Notification message body
|
|
* @param Timestamp $createdAt Creation timestamp
|
|
* @param array<string, mixed> $data Additional structured data
|
|
* @param array<NotificationChannel> $channels Delivery channels
|
|
* @param NotificationPriority $priority Delivery priority
|
|
* @param NotificationStatus $status Current status
|
|
* @param Timestamp|null $sentAt Delivery timestamp
|
|
* @param Timestamp|null $readAt Read timestamp
|
|
* @param string|null $actionUrl Optional action URL
|
|
* @param string|null $actionLabel Optional action button label
|
|
*/
|
|
public function __construct(
|
|
public NotificationId $id,
|
|
public string $recipientId,
|
|
public NotificationTypeInterface $type,
|
|
public string $title,
|
|
public string $body,
|
|
public Timestamp $createdAt,
|
|
public array $data = [],
|
|
public array $channels = [],
|
|
public NotificationPriority $priority = NotificationPriority::NORMAL,
|
|
public NotificationStatus $status = NotificationStatus::PENDING,
|
|
public ?Timestamp $sentAt = null,
|
|
public ?Timestamp $readAt = null,
|
|
public ?string $actionUrl = null,
|
|
public ?string $actionLabel = null
|
|
) {
|
|
if (empty($recipientId)) {
|
|
throw new \InvalidArgumentException('Recipient ID cannot be empty');
|
|
}
|
|
|
|
if (empty($title)) {
|
|
throw new \InvalidArgumentException('Title cannot be empty');
|
|
}
|
|
|
|
if (empty($body)) {
|
|
throw new \InvalidArgumentException('Body cannot be empty');
|
|
}
|
|
|
|
if (empty($channels)) {
|
|
throw new \InvalidArgumentException('At least one delivery channel is required');
|
|
}
|
|
}
|
|
|
|
public static function create(
|
|
string $recipientId,
|
|
NotificationTypeInterface $type,
|
|
string $title,
|
|
string $body,
|
|
NotificationChannel ...$channels
|
|
): self {
|
|
return new self(
|
|
id: NotificationId::generate(),
|
|
recipientId: $recipientId,
|
|
type: $type,
|
|
title: $title,
|
|
body: $body,
|
|
createdAt: Timestamp::now(),
|
|
data: [],
|
|
channels: $channels,
|
|
priority: NotificationPriority::NORMAL,
|
|
status: NotificationStatus::PENDING
|
|
);
|
|
}
|
|
|
|
public function withData(array $data): self
|
|
{
|
|
return new self(
|
|
id: $this->id,
|
|
recipientId: $this->recipientId,
|
|
type: $this->type,
|
|
title: $this->title,
|
|
body: $this->body,
|
|
createdAt: $this->createdAt,
|
|
data: [...$this->data, ...$data],
|
|
channels: $this->channels,
|
|
priority: $this->priority,
|
|
status: $this->status,
|
|
sentAt: $this->sentAt,
|
|
readAt: $this->readAt,
|
|
actionUrl: $this->actionUrl,
|
|
actionLabel: $this->actionLabel
|
|
);
|
|
}
|
|
|
|
public function withPriority(NotificationPriority $priority): self
|
|
{
|
|
return new self(
|
|
id: $this->id,
|
|
recipientId: $this->recipientId,
|
|
type: $this->type,
|
|
title: $this->title,
|
|
body: $this->body,
|
|
createdAt: $this->createdAt,
|
|
data: $this->data,
|
|
channels: $this->channels,
|
|
priority: $priority,
|
|
status: $this->status,
|
|
sentAt: $this->sentAt,
|
|
readAt: $this->readAt,
|
|
actionUrl: $this->actionUrl,
|
|
actionLabel: $this->actionLabel
|
|
);
|
|
}
|
|
|
|
public function withAction(string $url, string $label): self
|
|
{
|
|
return new self(
|
|
id: $this->id,
|
|
recipientId: $this->recipientId,
|
|
type: $this->type,
|
|
title: $this->title,
|
|
body: $this->body,
|
|
createdAt: $this->createdAt,
|
|
data: $this->data,
|
|
channels: $this->channels,
|
|
priority: $this->priority,
|
|
status: $this->status,
|
|
sentAt: $this->sentAt,
|
|
readAt: $this->readAt,
|
|
actionUrl: $url,
|
|
actionLabel: $label
|
|
);
|
|
}
|
|
|
|
public function markAsSent(): self
|
|
{
|
|
return new self(
|
|
id: $this->id,
|
|
recipientId: $this->recipientId,
|
|
type: $this->type,
|
|
title: $this->title,
|
|
body: $this->body,
|
|
createdAt: $this->createdAt,
|
|
data: $this->data,
|
|
channels: $this->channels,
|
|
priority: $this->priority,
|
|
status: NotificationStatus::SENT,
|
|
sentAt: Timestamp::now(),
|
|
readAt: $this->readAt,
|
|
actionUrl: $this->actionUrl,
|
|
actionLabel: $this->actionLabel
|
|
);
|
|
}
|
|
|
|
public function markAsDelivered(): self
|
|
{
|
|
return new self(
|
|
id: $this->id,
|
|
recipientId: $this->recipientId,
|
|
type: $this->type,
|
|
title: $this->title,
|
|
body: $this->body,
|
|
createdAt: $this->createdAt,
|
|
data: $this->data,
|
|
channels: $this->channels,
|
|
priority: $this->priority,
|
|
status: NotificationStatus::DELIVERED,
|
|
sentAt: $this->sentAt ?? Timestamp::now(),
|
|
readAt: $this->readAt,
|
|
actionUrl: $this->actionUrl,
|
|
actionLabel: $this->actionLabel
|
|
);
|
|
}
|
|
|
|
public function markAsRead(): self
|
|
{
|
|
return new self(
|
|
id: $this->id,
|
|
recipientId: $this->recipientId,
|
|
type: $this->type,
|
|
title: $this->title,
|
|
body: $this->body,
|
|
createdAt: $this->createdAt,
|
|
data: $this->data,
|
|
channels: $this->channels,
|
|
priority: $this->priority,
|
|
status: NotificationStatus::READ,
|
|
sentAt: $this->sentAt,
|
|
readAt: Timestamp::now(),
|
|
actionUrl: $this->actionUrl,
|
|
actionLabel: $this->actionLabel
|
|
);
|
|
}
|
|
|
|
public function markAsFailed(): self
|
|
{
|
|
return new self(
|
|
id: $this->id,
|
|
recipientId: $this->recipientId,
|
|
type: $this->type,
|
|
title: $this->title,
|
|
body: $this->body,
|
|
createdAt: $this->createdAt,
|
|
data: $this->data,
|
|
channels: $this->channels,
|
|
priority: $this->priority,
|
|
status: NotificationStatus::FAILED,
|
|
sentAt: $this->sentAt,
|
|
readAt: $this->readAt,
|
|
actionUrl: $this->actionUrl,
|
|
actionLabel: $this->actionLabel
|
|
);
|
|
}
|
|
|
|
public function isRead(): bool
|
|
{
|
|
return $this->status === NotificationStatus::READ;
|
|
}
|
|
|
|
public function hasAction(): bool
|
|
{
|
|
return $this->actionUrl !== null;
|
|
}
|
|
|
|
public function supportsChannel(NotificationChannel $channel): bool
|
|
{
|
|
foreach ($this->channels as $supportedChannel) {
|
|
if ($supportedChannel === $channel) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function toArray(): array
|
|
{
|
|
return [
|
|
'id' => $this->id->toString(),
|
|
'recipient_id' => $this->recipientId,
|
|
'type' => $this->type->toString(),
|
|
'title' => $this->title,
|
|
'body' => $this->body,
|
|
'data' => $this->data,
|
|
'channels' => array_map(fn ($c) => $c->value, $this->channels),
|
|
'priority' => $this->priority->value,
|
|
'status' => $this->status->value,
|
|
'created_at' => $this->createdAt->format('Y-m-d H:i:s'),
|
|
'sent_at' => $this->sentAt?->format('Y-m-d H:i:s'),
|
|
'read_at' => $this->readAt?->format('Y-m-d H:i:s'),
|
|
'action_url' => $this->actionUrl,
|
|
'action_label' => $this->actionLabel,
|
|
];
|
|
}
|
|
}
|