Centralize the current-timestamp SQL expression

"strftime('%Y-%m-%dT%H:%M:%SZ', 'now')" was a private nowExpr()
method copy-pasted identically in CardRepository, CardStatusRepository
and ProjectRepository, and inlined as a raw literal directly in
PasskeyRepository, WebAuthnChallengeRepository, UserRepository and
EmailVerificationRepository -- 7 files, ~12 occurrences of the same
string. Now one Database::nowExpr() static method (Support/Database
already being the natural home for SQLite-specific concerns), used
everywhere a repository sets a timestamp explicitly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 21:41:25 +01:00
co-authored by Claude Sonnet 5
parent 029c05c46f
commit accc6a273c
8 changed files with 37 additions and 38 deletions
+4 -8
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Repository; namespace App\Repository;
use App\Support\Database;
use PDO; use PDO;
/** /**
@@ -112,7 +113,7 @@ final class CardRepository
*/ */
public function update(int $id, array $fields): array public function update(int $id, array $fields): array
{ {
$sets = ['updated_at = ' . $this->nowExpr()]; $sets = ['updated_at = ' . Database::nowExpr()];
$params = ['id' => $id]; $params = ['id' => $id];
if (array_key_exists('text', $fields)) { if (array_key_exists('text', $fields)) {
@@ -185,7 +186,7 @@ final class CardRepository
$place = $this->pdo->prepare( $place = $this->pdo->prepare(
'UPDATE cards SET project_id = :project, status_id = :status, position = :position, 'UPDATE cards SET project_id = :project, status_id = :status, position = :position,
updated_at = ' . $this->nowExpr() . ' updated_at = ' . Database::nowExpr() . '
WHERE id = :id AND owner_id = :owner' WHERE id = :id AND owner_id = :owner'
); );
foreach ($orderedIds as $position => $id) { foreach ($orderedIds as $position => $id) {
@@ -229,7 +230,7 @@ final class CardRepository
$next = $this->nextPositionInColumn($ownerId, $projectId, $toStatusId); $next = $this->nextPositionInColumn($ownerId, $projectId, $toStatusId);
$stmt = $this->pdo->prepare( $stmt = $this->pdo->prepare(
'UPDATE cards SET status_id = :status, position = :position, updated_at = ' . $this->nowExpr() . ' 'UPDATE cards SET status_id = :status, position = :position, updated_at = ' . Database::nowExpr() . '
WHERE id = :id AND owner_id = :owner' WHERE id = :id AND owner_id = :owner'
); );
foreach ($ids as $i => $id) { foreach ($ids as $i => $id) {
@@ -362,11 +363,6 @@ final class CardRepository
return $row === false ? null : $this->cast($row); return $row === false ? null : $this->cast($row);
} }
private function nowExpr(): string
{
return "strftime('%Y-%m-%dT%H:%M:%SZ', 'now')";
}
/** /**
* @param array<string, mixed> $row * @param array<string, mixed> $row
* @return CardRow * @return CardRow
+2 -6
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Repository; namespace App\Repository;
use App\Support\Database;
use PDO; use PDO;
/** /**
@@ -100,7 +101,7 @@ final class CardStatusRepository
public function reorder(int $projectId, array $orderedIds): array public function reorder(int $projectId, array $orderedIds): array
{ {
$stmt = $this->pdo->prepare( $stmt = $this->pdo->prepare(
'UPDATE card_statuses SET position = :position, updated_at = ' . $this->nowExpr() . ' 'UPDATE card_statuses SET position = :position, updated_at = ' . Database::nowExpr() . '
WHERE id = :id AND project_id = :project' WHERE id = :id AND project_id = :project'
); );
foreach (array_values($orderedIds) as $position => $id) { foreach (array_values($orderedIds) as $position => $id) {
@@ -160,11 +161,6 @@ final class CardStatusRepository
return $row === false ? null : $this->cast($row); return $row === false ? null : $this->cast($row);
} }
private function nowExpr(): string
{
return "strftime('%Y-%m-%dT%H:%M:%SZ', 'now')";
}
/** /**
* @param array<string, mixed> $row * @param array<string, mixed> $row
* @return CardStatusRow * @return CardStatusRow
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Repository; namespace App\Repository;
use App\Support\Database;
use PDO; use PDO;
/** /**
@@ -70,8 +71,8 @@ final class EmailVerificationRepository
public function consume(int $id): bool public function consume(int $id): bool
{ {
$stmt = $this->pdo->prepare( $stmt = $this->pdo->prepare(
"UPDATE email_verifications SET consumed_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 'UPDATE email_verifications SET consumed_at = ' . Database::nowExpr() . '
WHERE id = :id AND consumed_at IS NULL" WHERE id = :id AND consumed_at IS NULL'
); );
$stmt->execute(['id' => $id]); $stmt->execute(['id' => $id]);
@@ -84,10 +85,10 @@ final class EmailVerificationRepository
public function pendingEmailFor(int $userId): ?string public function pendingEmailFor(int $userId): ?string
{ {
$stmt = $this->pdo->prepare( $stmt = $this->pdo->prepare(
"SELECT new_email FROM email_verifications 'SELECT new_email FROM email_verifications
WHERE user_id = :user AND new_email IS NOT NULL AND consumed_at IS NULL WHERE user_id = :user AND new_email IS NOT NULL AND consumed_at IS NULL
AND expires_at > strftime('%Y-%m-%dT%H:%M:%SZ', 'now') AND expires_at > ' . Database::nowExpr() . '
ORDER BY id DESC LIMIT 1" ORDER BY id DESC LIMIT 1'
); );
$stmt->execute(['user' => $userId]); $stmt->execute(['user' => $userId]);
+3 -2
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Repository; namespace App\Repository;
use App\Support\Database;
use PDO; use PDO;
/** /**
@@ -94,8 +95,8 @@ final class PasskeyRepository
public function markUsed(int $id, int $signCount): void public function markUsed(int $id, int $signCount): void
{ {
$this->pdo->prepare( $this->pdo->prepare(
"UPDATE passkeys SET sign_count = :count, last_used_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 'UPDATE passkeys SET sign_count = :count, last_used_at = ' . Database::nowExpr() . '
WHERE id = :id" WHERE id = :id'
)->execute(['count' => $signCount, 'id' => $id]); )->execute(['count' => $signCount, 'id' => $id]);
} }
+3 -7
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Repository; namespace App\Repository;
use App\Support\Database;
use PDO; use PDO;
/** /**
@@ -90,7 +91,7 @@ final class ProjectRepository
*/ */
public function update(int $id, int $ownerId, array $fields): array public function update(int $id, int $ownerId, array $fields): array
{ {
$sets = ['updated_at = ' . $this->nowExpr()]; $sets = ['updated_at = ' . Database::nowExpr()];
$params = ['id' => $id]; $params = ['id' => $id];
foreach (['title', 'description'] as $column) { foreach (['title', 'description'] as $column) {
@@ -117,15 +118,10 @@ final class ProjectRepository
/** Bump updated_at, e.g. when the project's cards change. */ /** Bump updated_at, e.g. when the project's cards change. */
public function touch(int $id): void public function touch(int $id): void
{ {
$this->pdo->prepare('UPDATE projects SET updated_at = ' . $this->nowExpr() . ' WHERE id = :id') $this->pdo->prepare('UPDATE projects SET updated_at = ' . Database::nowExpr() . ' WHERE id = :id')
->execute(['id' => $id]); ->execute(['id' => $id]);
} }
private function nowExpr(): string
{
return "strftime('%Y-%m-%dT%H:%M:%SZ', 'now')";
}
/** /**
* @param array<string, mixed> $row * @param array<string, mixed> $row
* @return ProjectRow * @return ProjectRow
+6 -8
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Repository; namespace App\Repository;
use App\Support\Database;
use PDO; use PDO;
use PDOException; use PDOException;
@@ -88,10 +89,9 @@ final class UserRepository
public function markEmailVerified(int $id): void public function markEmailVerified(int $id): void
{ {
$now = Database::nowExpr();
$this->pdo->prepare( $this->pdo->prepare(
"UPDATE users SET email_verified_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), "UPDATE users SET email_verified_at = {$now}, updated_at = {$now} WHERE id = :id"
updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
WHERE id = :id"
)->execute(['id' => $id]); )->execute(['id' => $id]);
} }
@@ -100,18 +100,16 @@ final class UserRepository
*/ */
public function updateEmail(int $id, string $email): void public function updateEmail(int $id, string $email): void
{ {
$now = Database::nowExpr();
$this->pdo->prepare( $this->pdo->prepare(
"UPDATE users SET email = :email, "UPDATE users SET email = :email, email_verified_at = {$now}, updated_at = {$now} WHERE id = :id"
email_verified_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now'),
updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
WHERE id = :id"
)->execute(['email' => $email, 'id' => $id]); )->execute(['email' => $email, 'id' => $id]);
} }
public function markVerificationEmailSent(int $id): void public function markVerificationEmailSent(int $id): void
{ {
$this->pdo->prepare( $this->pdo->prepare(
"UPDATE users SET verification_email_sent_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = :id" 'UPDATE users SET verification_email_sent_at = ' . Database::nowExpr() . ' WHERE id = :id'
)->execute(['id' => $id]); )->execute(['id' => $id]);
} }
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Repository; namespace App\Repository;
use App\Support\Database;
use PDO; use PDO;
/** /**
@@ -66,8 +67,8 @@ final class WebAuthnChallengeRepository
} }
$update = $this->pdo->prepare( $update = $this->pdo->prepare(
"UPDATE webauthn_challenges SET consumed_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 'UPDATE webauthn_challenges SET consumed_at = ' . Database::nowExpr() . '
WHERE id = :id AND consumed_at IS NULL" WHERE id = :id AND consumed_at IS NULL'
); );
$update->execute(['id' => $id]); $update->execute(['id' => $id]);
+10
View File
@@ -29,4 +29,14 @@ final class Database
{ {
return $this->pdo; return $this->pdo;
} }
/**
* SQL expression for the current UTC time, in the format every
* created_at/updated_at/*_at column uses (both as their DEFAULT in the
* migrations and wherever a repository sets one explicitly).
*/
public static function nowExpr(): string
{
return "strftime('%Y-%m-%dT%H:%M:%SZ', 'now')";
}
} }