From 21b6c583c083f7c0eb7eeea3bf47adc14d3163ff Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Sun, 21 Jun 2026 17:34:07 +0330 Subject: [PATCH] feat: add tag filtering to blog posts and implement JSON_CONTAINS DQL function --- config/packages/doctrine.yaml | 3 ++ docs/api/blog.md | 1 + src/Blog/Controller/BlogController.php | 5 ++-- src/Blog/Repository/BlogRepository.php | 32 ++++++++++++++++------ src/Shared/Doctrine/JsonContains.php | 38 ++++++++++++++++++++++++++ 5 files changed, 69 insertions(+), 10 deletions(-) create mode 100644 src/Shared/Doctrine/JsonContains.php diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 4ccc3a24..c126e408 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -18,6 +18,9 @@ doctrine: alias: App controller_resolver: auto_mapping: false + dql: + string_functions: + JSON_CONTAINS: App\Shared\Doctrine\JsonContains when@test: doctrine: diff --git a/docs/api/blog.md b/docs/api/blog.md index a6f2ba5f..c8e230ea 100644 --- a/docs/api/blog.md +++ b/docs/api/blog.md @@ -15,6 +15,7 @@ List published blog posts. |-------|------|----------|---------|-------------| | `page` | integer | ❌ | 1 | Page number | | `limit` | integer | ❌ | 20 | Items per page | +| `tag` | string | ❌ | — | Filter by exact tag **name** (e.g. `?tag=دیابت`). Blog tags are stored as a JSON array of names; only blogs whose `tags` array contains this exact name are returned. | ### Response `200` ```json diff --git a/src/Blog/Controller/BlogController.php b/src/Blog/Controller/BlogController.php index 58d9ed19..2503787a 100644 --- a/src/Blog/Controller/BlogController.php +++ b/src/Blog/Controller/BlogController.php @@ -60,9 +60,10 @@ class BlogController extends BaseController { $page = max(1, (int) $request->query->get('page', 1)); $limit = min(50, max(1, (int) $request->query->get('limit', 20))); + $tag = $request->query->get('tag') ?: null; - $blogs = array_map(fn(Blog $b) => $b->toListArray(), $this->blogRepo->findPublished($page, $limit)); - $total = $this->blogRepo->countPublished(); + $blogs = array_map(fn(Blog $b) => $b->toListArray(), $this->blogRepo->findPublished($page, $limit, $tag)); + $total = $this->blogRepo->countPublished($tag); return $this->paginated($blogs, $total, $page, $limit); } diff --git a/src/Blog/Repository/BlogRepository.php b/src/Blog/Repository/BlogRepository.php index dca86c17..b3097d84 100644 --- a/src/Blog/Repository/BlogRepository.php +++ b/src/Blog/Repository/BlogRepository.php @@ -14,24 +14,40 @@ class BlogRepository extends ServiceEntityRepository public function findBySlug(string $slug): ?Blog { return $this->findOneBy(['slug' => $slug]); } /** @return Blog[] published, newest first */ - public function findPublished(int $page = 1, int $limit = 20): array + public function findPublished(int $page = 1, int $limit = 20, ?string $tag = null): array { - return $this->createQueryBuilder('b') + $qb = $this->createQueryBuilder('b') ->where('b.status = :status') ->setParameter('status', Blog::STATUS_PUBLISHED) ->orderBy('b.createdAt', 'DESC') ->setFirstResult(($page - 1) * $limit) - ->setMaxResults($limit) - ->getQuery()->getResult(); + ->setMaxResults($limit); + + $this->applyTagFilter($qb, $tag); + + return $qb->getQuery()->getResult(); } - public function countPublished(): int + public function countPublished(?string $tag = null): int { - return (int) $this->createQueryBuilder('b') + $qb = $this->createQueryBuilder('b') ->select('COUNT(b.id)') ->where('b.status = :status') - ->setParameter('status', Blog::STATUS_PUBLISHED) - ->getQuery()->getSingleScalarResult(); + ->setParameter('status', Blog::STATUS_PUBLISHED); + + $this->applyTagFilter($qb, $tag); + + return (int) $qb->getQuery()->getSingleScalarResult(); + } + + private function applyTagFilter(\Doctrine\ORM\QueryBuilder $qb, ?string $tag): void + { + if ($tag === null || $tag === '') { + return; + } + // Blog.tags is a JSON array of tag names; match exact name membership. + $qb->andWhere('JSON_CONTAINS(b.tags, :tag) = 1') + ->setParameter('tag', json_encode($tag, JSON_UNESCAPED_UNICODE)); } public function save(Blog $e, bool $flush = true): void { $this->getEntityManager()->persist($e); if ($flush) $this->getEntityManager()->flush(); } diff --git a/src/Shared/Doctrine/JsonContains.php b/src/Shared/Doctrine/JsonContains.php new file mode 100644 index 00000000..75468795 --- /dev/null +++ b/src/Shared/Doctrine/JsonContains.php @@ -0,0 +1,38 @@ +match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + $this->jsonDoc = $parser->StringPrimary(); + $parser->match(TokenType::T_COMMA); + $this->candidate = $parser->StringPrimary(); + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } + + public function getSql(SqlWalker $sqlWalker): string + { + return sprintf( + 'JSON_CONTAINS(%s, %s)', + $this->jsonDoc->dispatch($sqlWalker), + $this->candidate->dispatch($sqlWalker) + ); + } +}