| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- <?php
- declare(strict_types=1);
- namespace App\Repositories;
- use App\Domain\Task;
- use PDO;
- use RuntimeException;
- final class TaskRepository
- {
- /** Whitelisted updatable columns. */
- private const UPDATABLE = ['title', 'owner_worker_id', 'priority'];
- public function __construct(private readonly PDO $pdo)
- {
- }
- /** @return list<Task> ordered by sort_order ASC */
- public function allForSprint(int $sprintId): array
- {
- $stmt = $this->pdo->prepare(
- 'SELECT * FROM tasks WHERE sprint_id = ? ORDER BY sort_order ASC'
- );
- $stmt->execute([$sprintId]);
- $out = [];
- foreach ($stmt as $row) {
- $out[] = self::hydrate($row);
- }
- return $out;
- }
- public function find(int $id): ?Task
- {
- $stmt = $this->pdo->prepare('SELECT * FROM tasks WHERE id = ?');
- $stmt->execute([$id]);
- $row = $stmt->fetch();
- return is_array($row) ? self::hydrate($row) : null;
- }
- public function create(
- int $sprintId,
- string $title,
- ?int $ownerWorkerId,
- int $priority,
- ): Task {
- $now = gmdate('Y-m-d\TH:i:s\Z');
- $max = (int) $this->pdo
- ->query('SELECT COALESCE(MAX(sort_order), 0) FROM tasks WHERE sprint_id = ' . $sprintId)
- ->fetchColumn();
- $stmt = $this->pdo->prepare(
- 'INSERT INTO tasks (sprint_id, title, owner_worker_id, priority, sort_order, created_at, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, ?)'
- );
- $stmt->execute([$sprintId, $title, $ownerWorkerId, $priority, $max + 1, $now, $now]);
- $id = (int) $this->pdo->lastInsertId();
- $task = $this->find($id);
- if ($task === null) {
- throw new RuntimeException('Inserted task not found');
- }
- return $task;
- }
- /**
- * @param array<string,mixed> $changes
- * @return array{before: Task, after: Task}
- */
- public function update(int $id, array $changes): array
- {
- $before = $this->find($id);
- if ($before === null) {
- throw new RuntimeException("Task {$id} not found");
- }
- $changes = array_intersect_key($changes, array_flip(self::UPDATABLE));
- if ($changes === []) {
- return ['before' => $before, 'after' => $before];
- }
- $sets = [];
- $vals = [];
- foreach ($changes as $col => $v) {
- $sets[] = "{$col} = ?";
- $vals[] = match ($col) {
- 'title' => (string) $v,
- 'owner_worker_id' => $v === null ? null : (int) $v,
- 'priority' => (int) $v,
- default => $v,
- };
- }
- $sets[] = 'updated_at = ?';
- $vals[] = gmdate('Y-m-d\TH:i:s\Z');
- $vals[] = $id;
- $stmt = $this->pdo->prepare(
- 'UPDATE tasks SET ' . implode(', ', $sets) . ' WHERE id = ?'
- );
- $stmt->execute($vals);
- $after = $this->find($id) ?? $before;
- return ['before' => $before, 'after' => $after];
- }
- /**
- * Delete a task. Does NOT read cascaded assignment rows; the controller
- * is responsible for auditing those BEFORE calling this method.
- * Returns the pre-deletion row for auditing.
- */
- public function delete(int $id): ?Task
- {
- $before = $this->find($id);
- if ($before === null) {
- return null;
- }
- $this->pdo
- ->prepare('DELETE FROM tasks WHERE id = ?')
- ->execute([$id]);
- return $before;
- }
- /**
- * Apply an ordering of tasks within a sprint. Same two-phase negate-then-
- * apply pattern as SprintWorkerRepository::reorder.
- *
- * @param list<array{task_id:int, sort_order:int}> $ordering
- * @return list<array{before: Task, after: Task}>
- */
- public function reorder(int $sprintId, array $ordering): array
- {
- if ($ordering === []) {
- return [];
- }
- $current = [];
- foreach ($this->allForSprint($sprintId) as $t) {
- $current[$t->id] = $t;
- }
- $stage = $this->pdo->prepare(
- 'UPDATE tasks SET sort_order = -? WHERE id = ? AND sprint_id = ?'
- );
- foreach ($ordering as $row) {
- $stage->execute([$row['sort_order'], $row['task_id'], $sprintId]);
- }
- $apply = $this->pdo->prepare(
- 'UPDATE tasks SET sort_order = ?, updated_at = ? WHERE id = ? AND sprint_id = ?'
- );
- $now = gmdate('Y-m-d\TH:i:s\Z');
- foreach ($ordering as $row) {
- $apply->execute([$row['sort_order'], $now, $row['task_id'], $sprintId]);
- }
- $out = [];
- foreach ($ordering as $row) {
- $tid = (int) $row['task_id'];
- $before = $current[$tid] ?? null;
- if ($before === null) {
- continue;
- }
- if ($before->sortOrder === (int) $row['sort_order']) {
- continue;
- }
- $after = $this->find($tid);
- if ($after !== null) {
- $out[] = ['before' => $before, 'after' => $after];
- }
- }
- return $out;
- }
- /**
- * @param array<string,mixed> $row
- */
- private static function hydrate(array $row): Task
- {
- return new Task(
- id: (int) $row['id'],
- sprintId: (int) $row['sprint_id'],
- title: (string) $row['title'],
- ownerWorkerId: isset($row['owner_worker_id']) && $row['owner_worker_id'] !== null
- ? (int) $row['owner_worker_id']
- : null,
- priority: (int) $row['priority'],
- sortOrder: (int) $row['sort_order'],
- createdAt: (string) $row['created_at'],
- updatedAt: (string) $row['updated_at'],
- );
- }
- }
|