*/ private array $metadata = []; private function __construct() { $this->uuid = $this->generateUuid(); $this->status = TaskStatus::PENDING; $this->type = TaskType::HUMAN_TASK; $this->createdAt = new \DateTimeImmutable(); $this->updatedAt = new \DateTimeImmutable(); } public static function create( string $title, string $createdBy, ?string $description = null, ?TaskType $type = null, ?int $parentTaskId = null, ?\DateTimeImmutable $dueDate = null ): self { $task = new self(); $task->title = $title; $task->createdBy = $createdBy; $task->description = $description; if ($type !== null) { $task->type = $type; } if ($parentTaskId !== null) { $task->parentTaskId = $parentTaskId; } if ($dueDate !== null) { $task->dueDate = $dueDate; } return $task; } private function generateUuid(): string { return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) ); } public function getId(): ?int { return $this->id; } public function getUuid(): string { return $this->uuid; } public function getTitle(): string { return $this->title; } public function getDescription(): ?string { return $this->description; } public function getType(): TaskType { return $this->type; } public function getStatus(): TaskStatus { return $this->status; } public function getCreatedBy(): string { return $this->createdBy; } public function getCreatedByType(): string { return $this->createdByType; } public function getParentTaskId(): ?int { return $this->parentTaskId; } public function getDueDate(): ?\DateTimeImmutable { return $this->dueDate; } public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; } public function getUpdatedAt(): \DateTimeImmutable { return $this->updatedAt; } public function getCompletedAt(): ?\DateTimeImmutable { return $this->completedAt; } /** @return array */ public function getMetadata(): array { return $this->metadata; } public function updateDetails(string $title, ?string $description = null): self { $this->title = $title; $this->description = $description; $this->touch(); return $this; } public function changeType(TaskType $type): self { $this->type = $type; $this->touch(); return $this; } public function assignParent(int $parentTaskId): self { $this->parentTaskId = $parentTaskId; $this->touch(); return $this; } public function setDueDateTo(\DateTimeImmutable $dueDate): self { $this->dueDate = $dueDate; $this->touch(); return $this; } public function clearDueDate(): self { $this->dueDate = null; $this->touch(); return $this; } /** @param array $metadata */ public function updateMetadata(array $metadata): self { $this->metadata = $metadata; $this->touch(); return $this; } public function isOverdue(): bool { return $this->dueDate !== null && !$this->status->isTerminal() && $this->dueDate < new \DateTimeImmutable(); } public function isSubtask(): bool { return $this->parentTaskId !== null; } public function start(): self { $this->transitionTo(TaskStatus::IN_PROGRESS); return $this; } public function complete(): self { $this->transitionTo(TaskStatus::COMPLETED); return $this; } public function fail(): self { $this->transitionTo(TaskStatus::FAILED); return $this; } public function cancel(): self { $this->transitionTo(TaskStatus::CANCELLED); return $this; } public function retry(): self { if ($this->status !== TaskStatus::FAILED) { throw new \DomainException('Can only retry failed tasks'); } $this->transitionTo(TaskStatus::PENDING); $this->completedAt = null; return $this; } private function transitionTo(TaskStatus $newStatus): void { if (!$this->status->canTransitionTo($newStatus)) { throw new \DomainException( sprintf('Cannot transition from %s to %s', $this->status->value, $newStatus->value) ); } $this->status = $newStatus; $this->touch(); if ($newStatus->isTerminal()) { $this->completedAt = new \DateTimeImmutable(); } } private function touch(): void { $this->updatedAt = new \DateTimeImmutable(); } public function setId(int $id): self { $this->id = $id; return $this; } /** @param array $data */ public function hydrate(array $data): self { $this->uuid = $data['uuid'] ?? $this->uuid; $this->title = $data['title'] ?? $this->title; $this->description = $data['description'] ?? null; $this->createdBy = $data['created_by'] ?? $this->createdBy; $this->createdByType = $data['created_by_type'] ?? 'human'; $this->parentTaskId = isset($data['parent_task_id']) ? (int) $data['parent_task_id'] : null; if (isset($data['type'])) { $this->type = is_string($data['type']) ? TaskType::from($data['type']) : $data['type']; } if (isset($data['status'])) { $this->status = is_string($data['status']) ? TaskStatus::from($data['status']) : $data['status']; } if (isset($data['due_date'])) { $this->dueDate = $this->toDateTime($data['due_date']); } if (isset($data['created_at'])) { $this->createdAt = $this->toDateTime($data['created_at']); } if (isset($data['updated_at'])) { $this->updatedAt = $this->toDateTime($data['updated_at']); } if (isset($data['completed_at'])) { $this->completedAt = $this->toDateTime($data['completed_at']); } if (isset($data['metadata'])) { $this->metadata = is_string($data['metadata']) ? json_decode($data['metadata'], true) ?? [] : $data['metadata']; } return $this; } private function toDateTime(\DateTimeImmutable|string $value): \DateTimeImmutable { return $value instanceof \DateTimeImmutable ? $value : new \DateTimeImmutable($value); } public function toArray(): array { return [ 'id' => $this->id, 'uuid' => $this->uuid, 'title' => $this->title, 'description' => $this->description, 'type' => $this->type->value, 'status' => $this->status->value, 'created_by' => $this->createdBy, 'created_by_type' => $this->createdByType, 'parent_task_id' => $this->parentTaskId, 'due_date' => $this->dueDate?->format('Y-m-d H:i:s'), 'created_at' => $this->createdAt->format('Y-m-d H:i:s.u'), 'updated_at' => $this->updatedAt->format('Y-m-d H:i:s.u'), 'completed_at' => $this->completedAt?->format('Y-m-d H:i:s.u'), 'metadata' => $this->metadata, ]; } public static function fromArray(array $data): self { $task = new self(); if (isset($data['id'])) { $task->setId((int) $data['id']); } return $task->hydrate($data); } }