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;
use App\Support\Database;
use PDO;
/**
@@ -112,7 +113,7 @@ final class CardRepository
*/
public function update(int $id, array $fields): array
{
$sets = ['updated_at = ' . $this->nowExpr()];
$sets = ['updated_at = ' . Database::nowExpr()];
$params = ['id' => $id];
if (array_key_exists('text', $fields)) {
@@ -185,7 +186,7 @@ final class CardRepository
$place = $this->pdo->prepare(
'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'
);
foreach ($orderedIds as $position => $id) {
@@ -229,7 +230,7 @@ final class CardRepository
$next = $this->nextPositionInColumn($ownerId, $projectId, $toStatusId);
$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'
);
foreach ($ids as $i => $id) {
@@ -362,11 +363,6 @@ final class CardRepository
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
* @return CardRow
+2 -6
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Repository;
use App\Support\Database;
use PDO;
/**
@@ -100,7 +101,7 @@ final class CardStatusRepository
public function reorder(int $projectId, array $orderedIds): array
{
$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'
);
foreach (array_values($orderedIds) as $position => $id) {
@@ -160,11 +161,6 @@ final class CardStatusRepository
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
* @return CardStatusRow
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Repository;
use App\Support\Database;
use PDO;
/**
@@ -70,8 +71,8 @@ final class EmailVerificationRepository
public function consume(int $id): bool
{
$stmt = $this->pdo->prepare(
"UPDATE email_verifications SET consumed_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
WHERE id = :id AND consumed_at IS NULL"
'UPDATE email_verifications SET consumed_at = ' . Database::nowExpr() . '
WHERE id = :id AND consumed_at IS NULL'
);
$stmt->execute(['id' => $id]);
@@ -84,10 +85,10 @@ final class EmailVerificationRepository
public function pendingEmailFor(int $userId): ?string
{
$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
AND expires_at > strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
ORDER BY id DESC LIMIT 1"
AND expires_at > ' . Database::nowExpr() . '
ORDER BY id DESC LIMIT 1'
);
$stmt->execute(['user' => $userId]);
+3 -2
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Repository;
use App\Support\Database;
use PDO;
/**
@@ -94,8 +95,8 @@ final class PasskeyRepository
public function markUsed(int $id, int $signCount): void
{
$this->pdo->prepare(
"UPDATE passkeys SET sign_count = :count, last_used_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
WHERE id = :id"
'UPDATE passkeys SET sign_count = :count, last_used_at = ' . Database::nowExpr() . '
WHERE id = :id'
)->execute(['count' => $signCount, 'id' => $id]);
}
+3 -7
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Repository;
use App\Support\Database;
use PDO;
/**
@@ -90,7 +91,7 @@ final class ProjectRepository
*/
public function update(int $id, int $ownerId, array $fields): array
{
$sets = ['updated_at = ' . $this->nowExpr()];
$sets = ['updated_at = ' . Database::nowExpr()];
$params = ['id' => $id];
foreach (['title', 'description'] as $column) {
@@ -117,15 +118,10 @@ final class ProjectRepository
/** Bump updated_at, e.g. when the project's cards change. */
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]);
}
private function nowExpr(): string
{
return "strftime('%Y-%m-%dT%H:%M:%SZ', 'now')";
}
/**
* @param array<string, mixed> $row
* @return ProjectRow
+6 -8
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Repository;
use App\Support\Database;
use PDO;
use PDOException;
@@ -88,10 +89,9 @@ final class UserRepository
public function markEmailVerified(int $id): void
{
$now = Database::nowExpr();
$this->pdo->prepare(
"UPDATE users SET email_verified_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now'),
updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
WHERE id = :id"
"UPDATE users SET email_verified_at = {$now}, updated_at = {$now} WHERE id = :id"
)->execute(['id' => $id]);
}
@@ -100,18 +100,16 @@ final class UserRepository
*/
public function updateEmail(int $id, string $email): void
{
$now = Database::nowExpr();
$this->pdo->prepare(
"UPDATE users SET email = :email,
email_verified_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now'),
updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
WHERE id = :id"
"UPDATE users SET email = :email, email_verified_at = {$now}, updated_at = {$now} WHERE id = :id"
)->execute(['email' => $email, 'id' => $id]);
}
public function markVerificationEmailSent(int $id): void
{
$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]);
}
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Repository;
use App\Support\Database;
use PDO;
/**
@@ -66,8 +67,8 @@ final class WebAuthnChallengeRepository
}
$update = $this->pdo->prepare(
"UPDATE webauthn_challenges SET consumed_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
WHERE id = :id AND consumed_at IS NULL"
'UPDATE webauthn_challenges SET consumed_at = ' . Database::nowExpr() . '
WHERE id = :id AND consumed_at IS NULL'
);
$update->execute(['id' => $id]);
+10
View File
@@ -29,4 +29,14 @@ final class Database
{
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')";
}
}