Refatoring : Working state

This commit is contained in:
julien
2026-03-16 14:11:49 +01:00
parent 073e23a9f8
commit d0761ff010
21 changed files with 1262 additions and 1301 deletions

View File

@@ -3,378 +3,12 @@ declare(strict_types=1);
namespace App\Post;
use PDO;
use App\Post\Infrastructure\PdoPostRepository;
final class PostRepository implements PostRepositoryInterface
/**
* Pont de compatibilité : l'implémentation PDO principale vit désormais dans
* App\Post\Infrastructure\PdoPostRepository.
*/
final class PostRepository extends PdoPostRepository implements PostRepositoryInterface
{
private const SELECT = '
SELECT posts.id, posts.title, posts.content, posts.slug,
posts.author_id, posts.category_id, posts.created_at, posts.updated_at,
users.username AS author_username,
categories.name AS category_name,
categories.slug AS category_slug
FROM posts
LEFT JOIN users ON users.id = posts.author_id
LEFT JOIN categories ON categories.id = posts.category_id
';
public function __construct(private readonly PDO $db)
{
}
public function findAll(?int $categoryId = null): array
{
if ($categoryId === null) {
$stmt = $this->db->query(self::SELECT . ' ORDER BY posts.id DESC');
if ($stmt === false) {
throw new \RuntimeException('La requête SELECT sur posts a échoué.');
}
return $this->hydratePosts($stmt->fetchAll(PDO::FETCH_ASSOC));
}
$stmt = $this->db->prepare(self::SELECT . ' WHERE posts.category_id = :category_id ORDER BY posts.id DESC');
$stmt->execute([':category_id' => $categoryId]);
return $this->hydratePosts($stmt->fetchAll(PDO::FETCH_ASSOC));
}
public function findPage(int $limit, int $offset, ?int $categoryId = null): array
{
$sql = self::SELECT;
$params = [];
if ($categoryId !== null) {
$sql .= ' WHERE posts.category_id = :category_id';
$params[':category_id'] = $categoryId;
}
$sql .= ' ORDER BY posts.id DESC LIMIT :limit OFFSET :offset';
$stmt = $this->db->prepare($sql);
$this->bindParams($stmt, $params);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
return $this->hydratePosts($stmt->fetchAll(PDO::FETCH_ASSOC));
}
public function countAll(?int $categoryId = null): int
{
if ($categoryId === null) {
$stmt = $this->db->query('SELECT COUNT(*) FROM posts');
if ($stmt === false) {
throw new \RuntimeException('La requête COUNT sur posts a échoué.');
}
return (int) ($stmt->fetchColumn() ?: 0);
}
$stmt = $this->db->prepare('SELECT COUNT(*) FROM posts WHERE category_id = :category_id');
$stmt->execute([':category_id' => $categoryId]);
return (int) $stmt->fetchColumn();
}
public function findRecent(int $limit): array
{
$stmt = $this->db->prepare(self::SELECT . ' ORDER BY posts.id DESC LIMIT :limit');
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
return $this->hydratePosts($stmt->fetchAll(PDO::FETCH_ASSOC));
}
public function findByUserId(int $userId, ?int $categoryId = null): array
{
$sql = self::SELECT . ' WHERE posts.author_id = :author_id';
$params = [':author_id' => $userId];
if ($categoryId !== null) {
$sql .= ' AND posts.category_id = :category_id';
$params[':category_id'] = $categoryId;
}
$sql .= ' ORDER BY posts.id DESC';
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return $this->hydratePosts($stmt->fetchAll(PDO::FETCH_ASSOC));
}
public function findByUserPage(int $userId, int $limit, int $offset, ?int $categoryId = null): array
{
$sql = self::SELECT . ' WHERE posts.author_id = :author_id';
$params = [':author_id' => $userId];
if ($categoryId !== null) {
$sql .= ' AND posts.category_id = :category_id';
$params[':category_id'] = $categoryId;
}
$sql .= ' ORDER BY posts.id DESC LIMIT :limit OFFSET :offset';
$stmt = $this->db->prepare($sql);
$this->bindParams($stmt, $params);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
return $this->hydratePosts($stmt->fetchAll(PDO::FETCH_ASSOC));
}
public function countByUserId(int $userId, ?int $categoryId = null): int
{
$sql = 'SELECT COUNT(*) FROM posts WHERE author_id = :author_id';
$params = [':author_id' => $userId];
if ($categoryId !== null) {
$sql .= ' AND category_id = :category_id';
$params[':category_id'] = $categoryId;
}
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return (int) $stmt->fetchColumn();
}
public function findBySlug(string $slug): ?Post
{
$stmt = $this->db->prepare(self::SELECT . ' WHERE posts.slug = :slug');
$stmt->execute([':slug' => $slug]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ? Post::fromArray($row) : null;
}
public function findById(int $id): ?Post
{
$stmt = $this->db->prepare(self::SELECT . ' WHERE posts.id = :id');
$stmt->execute([':id' => $id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ? Post::fromArray($row) : null;
}
public function create(Post $post, string $slug, int $authorId, ?int $categoryId): int
{
$stmt = $this->db->prepare(
'INSERT INTO posts (title, content, slug, author_id, category_id, created_at, updated_at)
VALUES (:title, :content, :slug, :author_id, :category_id, :created_at, :updated_at)'
);
$stmt->execute([
':title' => $post->getTitle(),
':content' => $post->getContent(),
':slug' => $slug,
':author_id' => $authorId,
':category_id' => $categoryId,
':created_at' => date('Y-m-d H:i:s'),
':updated_at' => date('Y-m-d H:i:s'),
]);
return (int) $this->db->lastInsertId();
}
public function update(int $id, Post $post, string $slug, ?int $categoryId): int
{
$stmt = $this->db->prepare(
'UPDATE posts
SET title = :title, content = :content, slug = :slug,
category_id = :category_id, updated_at = :updated_at
WHERE id = :id'
);
$stmt->execute([
':title' => $post->getTitle(),
':content' => $post->getContent(),
':slug' => $slug,
':category_id' => $categoryId,
':updated_at' => date('Y-m-d H:i:s'),
':id' => $id,
]);
return $stmt->rowCount();
}
public function delete(int $id): int
{
$stmt = $this->db->prepare('DELETE FROM posts WHERE id = :id');
$stmt->execute([':id' => $id]);
return $stmt->rowCount();
}
public function search(string $query, ?int $categoryId = null, ?int $authorId = null): array
{
$ftsQuery = $this->buildFtsQuery($query);
if ($ftsQuery === '') {
return [];
}
[$sql, $params] = $this->buildSearchSql($ftsQuery, $categoryId, $authorId);
$sql .= ' ORDER BY rank';
$stmt = $this->db->prepare($sql);
$this->bindParams($stmt, $params);
$stmt->execute();
return $this->hydratePosts($stmt->fetchAll(PDO::FETCH_ASSOC));
}
public function searchPage(string $query, int $limit, int $offset, ?int $categoryId = null, ?int $authorId = null): array
{
$ftsQuery = $this->buildFtsQuery($query);
if ($ftsQuery === '') {
return [];
}
[$sql, $params] = $this->buildSearchSql($ftsQuery, $categoryId, $authorId);
$sql .= ' ORDER BY rank LIMIT :limit OFFSET :offset';
$stmt = $this->db->prepare($sql);
$this->bindParams($stmt, $params);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
return $this->hydratePosts($stmt->fetchAll(PDO::FETCH_ASSOC));
}
public function countSearch(string $query, ?int $categoryId = null, ?int $authorId = null): int
{
$ftsQuery = $this->buildFtsQuery($query);
if ($ftsQuery === '') {
return 0;
}
$sql = '
SELECT COUNT(*)
FROM posts_fts f
JOIN posts p ON p.id = f.rowid
WHERE posts_fts MATCH :query
';
$params = [':query' => $ftsQuery];
if ($categoryId !== null) {
$sql .= ' AND p.category_id = :category_id';
$params[':category_id'] = $categoryId;
}
if ($authorId !== null) {
$sql .= ' AND p.author_id = :author_id';
$params[':author_id'] = $authorId;
}
$stmt = $this->db->prepare($sql);
$this->bindParams($stmt, $params);
$stmt->execute();
return (int) $stmt->fetchColumn();
}
public function slugExists(string $slug, ?int $excludeId = null): bool
{
$stmt = $this->db->prepare('SELECT id FROM posts WHERE slug = :slug');
$stmt->execute([':slug' => $slug]);
$existingId = $stmt->fetchColumn();
if ($existingId === false) {
return false;
}
$existingId = (int) $existingId;
return $excludeId !== null ? $existingId !== $excludeId : true;
}
public function countByEmbeddedMediaUrl(string $url): int
{
$stmt = $this->db->prepare('SELECT COUNT(*) FROM posts WHERE instr(content, :url) > 0');
$stmt->execute([':url' => $url]);
return (int) $stmt->fetchColumn();
}
public function findByEmbeddedMediaUrl(string $url, int $limit = 5): array
{
$stmt = $this->db->prepare(
self::SELECT . ' WHERE instr(posts.content, :url) > 0 ORDER BY posts.updated_at DESC LIMIT :limit'
);
$stmt->bindValue(':url', $url, PDO::PARAM_STR);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
return $this->hydratePosts($stmt->fetchAll(PDO::FETCH_ASSOC));
}
/**
* @return array{0:string,1:array<string,mixed>}
*/
private function buildSearchSql(string $ftsQuery, ?int $categoryId = null, ?int $authorId = null): array
{
$sql = '
SELECT p.id, p.title, p.content, p.slug,
p.author_id, p.category_id, p.created_at, p.updated_at,
u.username AS author_username,
c.name AS category_name,
c.slug AS category_slug
FROM posts_fts f
JOIN posts p ON p.id = f.rowid
LEFT JOIN users u ON u.id = p.author_id
LEFT JOIN categories c ON c.id = p.category_id
WHERE posts_fts MATCH :query
';
$params = [':query' => $ftsQuery];
if ($categoryId !== null) {
$sql .= ' AND p.category_id = :category_id';
$params[':category_id'] = $categoryId;
}
if ($authorId !== null) {
$sql .= ' AND p.author_id = :author_id';
$params[':author_id'] = $authorId;
}
return [$sql, $params];
}
private function buildFtsQuery(string $input): string
{
$words = preg_split('/\s+/', trim($input), -1, PREG_SPLIT_NO_EMPTY) ?: [];
if (empty($words)) {
return '';
}
$terms = array_map(
fn ($w) => '"' . str_replace('"', '""', $w) . '"*',
$words
);
return implode(' ', $terms);
}
/**
* @param array<string, mixed> $params
*/
private function bindParams(\PDOStatement $stmt, array $params): void
{
foreach ($params as $key => $value) {
$stmt->bindValue($key, $value, is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR);
}
}
/**
* @param array<int, array<string, mixed>> $rows
* @return Post[]
*/
private function hydratePosts(array $rows): array
{
return array_map(fn ($row) => Post::fromArray($row), $rows);
}
}