feat(store): finish ContributionStore and add typed entities
- Fix add()'s upsert SQL (was invalid SQL from typos) and use it for the new merge() as well. - Fix remove()'s missing FROM keyword and drop the unindexed LIKE scan in favor of an exact match. - Add latestDate(), all() with optional sinceDays filtering, and prune() with a retention-day cutoff. - Add Contribution (immutable value object) and ContributionCollection (IteratorAggregate + Countable) so all() returns something typed instead of a raw date => count array. - Cover all of the above with unit tests against an in-memory SQLite database.
This commit is contained in:
@@ -4,24 +4,30 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\Contribution;
|
||||
use App\Entity\ContributionCollection;
|
||||
use PDO;
|
||||
|
||||
class ContributionStore {
|
||||
class ContributionStore
|
||||
{
|
||||
private PDO $pdo;
|
||||
private const int DAY_IN_SECONDS = 86400;
|
||||
|
||||
public function __construct(
|
||||
private readonly string $dbPath = "%kernel.project_dir%/var/data/contributions.db",
|
||||
private readonly ?int $retentionDays = null,
|
||||
){
|
||||
if(!is_dir(\dirname($this->dbPath))){
|
||||
mkdir(\dirname($this->dbPath), 0755, recursive: true);
|
||||
) {
|
||||
$dir = \dirname($this->dbPath);
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0755, recursive: true);
|
||||
}
|
||||
$this->pdo = new PDO('sqlite:' . $this->dbPath);
|
||||
$this->pdo->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
|
||||
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$this->prepareSchema();
|
||||
}
|
||||
|
||||
private function prepareSchema():void{
|
||||
private function prepareSchema(): void
|
||||
{
|
||||
$this->pdo->exec('
|
||||
CREATE TABLE IF NOT EXISTS contributions (
|
||||
provider TEXT NOT NULL,
|
||||
@@ -32,28 +38,74 @@ class ContributionStore {
|
||||
');
|
||||
}
|
||||
|
||||
public function add(string $provider, int $unixtime, int $count):void{
|
||||
$stmt = $this->pdo->prepare('
|
||||
INSERT INTO contributions (
|
||||
provider, date, count
|
||||
)
|
||||
VALUES(
|
||||
?,?,?
|
||||
)
|
||||
public function add(string $provider, int $unixtime, int $count): void
|
||||
{
|
||||
$stmt = $this->pdo->prepare('
|
||||
INSERT INTO contributions (provider, date, count)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(provider, date) DO UPDATE SET count = excluded.count
|
||||
');
|
||||
$stmt->execute([$provider,$unixtime,$count]);
|
||||
$stmt->execute([$provider, $unixtime, $count]);
|
||||
}
|
||||
|
||||
public function remove(string $provider, int $unixtime):void{
|
||||
$stmt = $this->pdo->prepare('
|
||||
DELETE contributions
|
||||
WHERE provider LIKE ? and date = ?
|
||||
public function remove(string $provider, int $unixtime): void
|
||||
{
|
||||
$stmt = $this->pdo->prepare('
|
||||
DELETE FROM contributions
|
||||
WHERE provider = ? AND date = ?
|
||||
');
|
||||
$stmt->execute([$provider,$unixtime]);
|
||||
$stmt->execute([$provider, $unixtime]);
|
||||
}
|
||||
|
||||
public function all():void{
|
||||
/**
|
||||
* @param array<int, int> $dateCounts unix timestamp => count
|
||||
*/
|
||||
public function merge(string $provider, array $dateCounts): void
|
||||
{
|
||||
foreach ($dateCounts as $unixtime => $count) {
|
||||
$this->add($provider, $unixtime, $count);
|
||||
}
|
||||
}
|
||||
|
||||
public function latestDate(string $provider): ?int
|
||||
{
|
||||
$stmt = $this->pdo->prepare('SELECT MAX(date) FROM contributions WHERE provider = ?');
|
||||
$stmt->execute([$provider]);
|
||||
$result = $stmt->fetchColumn();
|
||||
|
||||
return $result !== null ? (int) $result : null;
|
||||
}
|
||||
|
||||
public function all(string $provider, ?int $sinceDays = null): ContributionCollection
|
||||
{
|
||||
$sql = 'SELECT provider, date, count FROM contributions WHERE provider = ?';
|
||||
$params = [$provider];
|
||||
|
||||
if ($sinceDays !== null) {
|
||||
$sql .= ' AND date >= ?';
|
||||
$params[] = time() - $sinceDays * self::DAY_IN_SECONDS;
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
|
||||
$contributions = array_map(
|
||||
static fn (array $row): Contribution => new Contribution($row['provider'], (int) $row['date'], (int) $row['count']),
|
||||
$stmt->fetchAll(PDO::FETCH_ASSOC),
|
||||
);
|
||||
|
||||
return new ContributionCollection(...$contributions);
|
||||
}
|
||||
|
||||
public function prune(): void
|
||||
{
|
||||
if ($this->retentionDays === null || $this->retentionDays === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cutoff = time() - $this->retentionDays * self::DAY_IN_SECONDS;
|
||||
|
||||
$stmt = $this->pdo->prepare('DELETE FROM contributions WHERE date < ?');
|
||||
$stmt->execute([$cutoff]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user