SprintWorkerRepository.php 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Repositories;
  4. use App\Domain\SprintWorker;
  5. use PDO;
  6. use RuntimeException;
  7. final class SprintWorkerRepository
  8. {
  9. public function __construct(private readonly PDO $pdo)
  10. {
  11. }
  12. /** @return list<SprintWorker> ordered by sort_order ASC, with worker name joined. */
  13. public function allForSprint(int $sprintId): array
  14. {
  15. $stmt = $this->pdo->prepare(
  16. 'SELECT sw.*, w.name AS worker_name
  17. FROM sprint_workers sw
  18. JOIN workers w ON w.id = sw.worker_id
  19. WHERE sw.sprint_id = ?
  20. ORDER BY sw.sort_order ASC'
  21. );
  22. $stmt->execute([$sprintId]);
  23. $out = [];
  24. foreach ($stmt as $row) {
  25. $out[] = self::hydrate($row);
  26. }
  27. return $out;
  28. }
  29. public function find(int $id): ?SprintWorker
  30. {
  31. $stmt = $this->pdo->prepare(
  32. 'SELECT sw.*, w.name AS worker_name
  33. FROM sprint_workers sw
  34. JOIN workers w ON w.id = sw.worker_id
  35. WHERE sw.id = ?'
  36. );
  37. $stmt->execute([$id]);
  38. $row = $stmt->fetch();
  39. return is_array($row) ? self::hydrate($row) : null;
  40. }
  41. /**
  42. * Add a worker to a sprint at the end of the order. Returns the created
  43. * SprintWorker. UNIQUE(sprint_id, worker_id) enforces that a worker
  44. * can't be added twice; PDOException propagates.
  45. */
  46. public function add(int $sprintId, int $workerId, float $rtb): SprintWorker
  47. {
  48. $maxOrder = (int) $this->pdo
  49. ->query('SELECT COALESCE(MAX(sort_order), 0) FROM sprint_workers WHERE sprint_id = ' . $sprintId)
  50. ->fetchColumn();
  51. $newOrder = $maxOrder + 1;
  52. $stmt = $this->pdo->prepare(
  53. 'INSERT INTO sprint_workers (sprint_id, worker_id, rtb, sort_order) VALUES (?, ?, ?, ?)'
  54. );
  55. $stmt->execute([$sprintId, $workerId, $rtb, $newOrder]);
  56. $id = (int) $this->pdo->lastInsertId();
  57. $sw = $this->find($id);
  58. if ($sw === null) {
  59. throw new RuntimeException('Inserted sprint_worker not found');
  60. }
  61. return $sw;
  62. }
  63. /**
  64. * Remove a sprint_worker row. Returns the removed row (before) for
  65. * auditing, or null if it didn't exist.
  66. */
  67. public function remove(int $id): ?SprintWorker
  68. {
  69. $before = $this->find($id);
  70. if ($before === null) {
  71. return null;
  72. }
  73. $this->pdo
  74. ->prepare('DELETE FROM sprint_workers WHERE id = ?')
  75. ->execute([$id]);
  76. return $before;
  77. }
  78. /**
  79. * Apply an ordering of sprint_workers within a single sprint. The
  80. * $ordering payload is a list of {sprint_worker_id, sort_order} pairs,
  81. * assumed to be self-consistent (no duplicate orders, all IDs belong to
  82. * the same sprint).
  83. *
  84. * Returns per-row before/after for auditing. Unchanged rows are omitted
  85. * (the audit logger's no-op rule would drop them anyway, but this also
  86. * avoids unnecessary UPDATE statements).
  87. *
  88. * @param list<array{sprint_worker_id:int, sort_order:int}> $ordering
  89. * @return list<array{before: SprintWorker, after: SprintWorker}>
  90. */
  91. public function reorder(int $sprintId, array $ordering): array
  92. {
  93. if ($ordering === []) {
  94. return [];
  95. }
  96. // Pre-fetch current state for the sprint so we can diff before/after.
  97. $current = [];
  98. foreach ($this->allForSprint($sprintId) as $sw) {
  99. $current[$sw->id] = $sw;
  100. }
  101. // Stage new sort orders into negative space first so the updates don't
  102. // violate any hypothetical unique constraint (the schema doesn't have
  103. // one on sort_order today, but this keeps us future-proof).
  104. $stage = $this->pdo->prepare(
  105. 'UPDATE sprint_workers SET sort_order = -? WHERE id = ? AND sprint_id = ?'
  106. );
  107. foreach ($ordering as $row) {
  108. $stage->execute([$row['sort_order'], $row['sprint_worker_id'], $sprintId]);
  109. }
  110. $apply = $this->pdo->prepare(
  111. 'UPDATE sprint_workers SET sort_order = ? WHERE id = ? AND sprint_id = ?'
  112. );
  113. foreach ($ordering as $row) {
  114. $apply->execute([$row['sort_order'], $row['sprint_worker_id'], $sprintId]);
  115. }
  116. $out = [];
  117. foreach ($ordering as $row) {
  118. $swId = (int) $row['sprint_worker_id'];
  119. $before = $current[$swId] ?? null;
  120. if ($before === null) {
  121. continue;
  122. }
  123. if ($before->sortOrder === (int) $row['sort_order']) {
  124. continue;
  125. }
  126. $after = $this->find($swId);
  127. if ($after !== null) {
  128. $out[] = ['before' => $before, 'after' => $after];
  129. }
  130. }
  131. return $out;
  132. }
  133. /**
  134. * Edit the RTB for a single sprint_worker. Returns before/after.
  135. *
  136. * @return array{before: SprintWorker, after: SprintWorker}
  137. */
  138. public function setRtb(int $id, float $rtb): array
  139. {
  140. $before = $this->find($id);
  141. if ($before === null) {
  142. throw new RuntimeException("sprint_worker {$id} not found");
  143. }
  144. $this->pdo
  145. ->prepare('UPDATE sprint_workers SET rtb = ? WHERE id = ?')
  146. ->execute([$rtb, $id]);
  147. $after = $this->find($id) ?? $before;
  148. return ['before' => $before, 'after' => $after];
  149. }
  150. /**
  151. * @param array<string,mixed> $row
  152. */
  153. private static function hydrate(array $row): SprintWorker
  154. {
  155. return new SprintWorker(
  156. id: (int) $row['id'],
  157. sprintId: (int) $row['sprint_id'],
  158. workerId: (int) $row['worker_id'],
  159. workerName: (string) $row['worker_name'],
  160. rtb: (float) $row['rtb'],
  161. sortOrder: (int) $row['sort_order'],
  162. );
  163. }
  164. }