WorkerRepository.php 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Repositories;
  4. use App\Domain\Worker;
  5. use PDO;
  6. use RuntimeException;
  7. final class WorkerRepository
  8. {
  9. /** Whitelisted updatable columns. Do not extend without also auditing intent. */
  10. private const UPDATABLE = ['name', 'is_active', 'default_rtb'];
  11. public function __construct(private readonly PDO $pdo)
  12. {
  13. }
  14. /** @return list<Worker> */
  15. public function all(): array
  16. {
  17. $stmt = $this->pdo->query(
  18. 'SELECT * FROM workers ORDER BY is_active DESC, LOWER(name) ASC'
  19. );
  20. $out = [];
  21. foreach ($stmt as $row) {
  22. $out[] = self::hydrate($row);
  23. }
  24. return $out;
  25. }
  26. public function find(int $id): ?Worker
  27. {
  28. $stmt = $this->pdo->prepare('SELECT * FROM workers WHERE id = ?');
  29. $stmt->execute([$id]);
  30. $row = $stmt->fetch();
  31. return is_array($row) ? self::hydrate($row) : null;
  32. }
  33. public function findByName(string $name): ?Worker
  34. {
  35. $stmt = $this->pdo->prepare('SELECT * FROM workers WHERE name = ?');
  36. $stmt->execute([$name]);
  37. $row = $stmt->fetch();
  38. return is_array($row) ? self::hydrate($row) : null;
  39. }
  40. /**
  41. * Active workers that are NOT yet members of the given sprint. Used by
  42. * the sprint-settings page to populate the "available" list.
  43. *
  44. * @return list<Worker>
  45. */
  46. public function activeNotInSprint(int $sprintId): array
  47. {
  48. $stmt = $this->pdo->prepare(
  49. 'SELECT w.* FROM workers w
  50. WHERE w.is_active = 1
  51. AND w.id NOT IN (
  52. SELECT sw.worker_id FROM sprint_workers sw WHERE sw.sprint_id = ?
  53. )
  54. ORDER BY LOWER(w.name) ASC'
  55. );
  56. $stmt->execute([$sprintId]);
  57. $out = [];
  58. foreach ($stmt as $row) {
  59. $out[] = self::hydrate($row);
  60. }
  61. return $out;
  62. }
  63. /**
  64. * Insert a new worker. Throws if the unique name constraint is violated.
  65. */
  66. public function create(string $name, bool $isActive, float $defaultRtb): Worker
  67. {
  68. $now = gmdate('Y-m-d\TH:i:s\Z');
  69. $stmt = $this->pdo->prepare(
  70. 'INSERT INTO workers (name, is_active, default_rtb, created_at, updated_at)
  71. VALUES (?, ?, ?, ?, ?)'
  72. );
  73. $stmt->execute([$name, $isActive ? 1 : 0, $defaultRtb, $now, $now]);
  74. $id = (int) $this->pdo->lastInsertId();
  75. $worker = $this->find($id);
  76. if ($worker === null) {
  77. throw new RuntimeException('Inserted worker not found');
  78. }
  79. return $worker;
  80. }
  81. /**
  82. * Apply the given changes (only whitelisted columns). Returns before/after
  83. * snapshots so the caller can drive the AuditLogger.
  84. *
  85. * @param array<string,mixed> $changes subset of {name, is_active, default_rtb}
  86. * @return array{before: Worker, after: Worker}
  87. */
  88. public function update(int $id, array $changes): array
  89. {
  90. $before = $this->find($id);
  91. if ($before === null) {
  92. throw new RuntimeException("Worker {$id} not found");
  93. }
  94. $changes = array_intersect_key($changes, array_flip(self::UPDATABLE));
  95. if ($changes === []) {
  96. return ['before' => $before, 'after' => $before];
  97. }
  98. $sets = [];
  99. $vals = [];
  100. foreach ($changes as $col => $v) {
  101. $sets[] = "{$col} = ?";
  102. if ($col === 'is_active') {
  103. $vals[] = (bool) $v ? 1 : 0;
  104. } elseif ($col === 'default_rtb') {
  105. $vals[] = (float) $v;
  106. } else {
  107. $vals[] = (string) $v;
  108. }
  109. }
  110. $sets[] = 'updated_at = ?';
  111. $vals[] = gmdate('Y-m-d\TH:i:s\Z');
  112. $vals[] = $id;
  113. $stmt = $this->pdo->prepare(
  114. 'UPDATE workers SET ' . implode(', ', $sets) . ' WHERE id = ?'
  115. );
  116. $stmt->execute($vals);
  117. $after = $this->find($id) ?? $before;
  118. return ['before' => $before, 'after' => $after];
  119. }
  120. /**
  121. * @param array<string,mixed> $row
  122. */
  123. private static function hydrate(array $row): Worker
  124. {
  125. return new Worker(
  126. id: (int) $row['id'],
  127. name: (string) $row['name'],
  128. isActive: ((int) $row['is_active']) === 1,
  129. defaultRtb: (float) $row['default_rtb'],
  130. createdAt: (string) $row['created_at'],
  131. updatedAt: (string) $row['updated_at'],
  132. );
  133. }
  134. }