Refatoring : Working state
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user