diff --git a/src/Filter/Date.php b/src/Filter/Date.php index 1a81eb5abf3bb0be1633686d38fdc13d0d53e610..7d54ba51e4248e27fa4e35a00117da8a5eb0ecca 100644 --- a/src/Filter/Date.php +++ b/src/Filter/Date.php @@ -19,7 +19,7 @@ final class Date extends HttpFilter return $queryBuilder; } - $queryBuilder->where( + $queryBuilder->andWhere( $this->getColumn() . ' BETWEEN :dateStart AND :dateEnd', ); diff --git a/src/Filter/DateRange.php b/src/Filter/DateRange.php index 6456257dbc20338bfbeeb947a48d62577e3052e9..78a0af05d1ce346a3c50b915dabed4a9b685c485 100644 --- a/src/Filter/DateRange.php +++ b/src/Filter/DateRange.php @@ -27,7 +27,7 @@ final class DateRange extends HttpFilter } if ($fromDate) { - $queryBuilder->where($this->getColumn() . ' >= :fromDate') + $queryBuilder->andWhere($this->getColumn() . ' >= :fromDate') ->setParameter('fromDate', $fromDate->setTime(0, 0, 0)); } diff --git a/src/Filter/ILike.php b/src/Filter/ILike.php index 2fe7a3ebe398358d80c4daabf7a4bf6d4dab6381..a49fb34e0829cdd6653537adcadb1ad35354ecd5 100644 --- a/src/Filter/ILike.php +++ b/src/Filter/ILike.php @@ -11,7 +11,7 @@ final class ILike extends HttpFilter { public function addToQuery(QueryBuilder $queryBuilder): QueryBuilder { - $queryBuilder->where( + $queryBuilder->andWhere( $this->getColumn() . ' ILIKE \'%' . $this->getHttpValue() . '%\'', ); diff --git a/src/Filter/In.php b/src/Filter/In.php index 946e38fcee73e370ac1bf9701821ca1e60556f6b..8a90d2da0e51a0ea4fc7f6a4a013926b21535133 100644 --- a/src/Filter/In.php +++ b/src/Filter/In.php @@ -18,7 +18,7 @@ final class In extends HttpFilter $values = array_map(fn($value) => '\'' . $value . '\'', $this->getHttpValue()); $stringValues = implode(',', $values); - $queryBuilder->where( + $queryBuilder->andWhere( $this->getColumn() . ' IN (' . $stringValues . ')', ); diff --git a/src/Filter/Like.php b/src/Filter/Like.php index 30dd2ceca3a6efc941dd83e0c0f8cb205ca77ac9..a5dc19aa09e562280db60761eef09a16c35b03b7 100644 --- a/src/Filter/Like.php +++ b/src/Filter/Like.php @@ -15,7 +15,7 @@ final class Like extends HttpFilter return $queryBuilder; } - $queryBuilder->where( + $queryBuilder->andWhere( $this->getColumn() . ' LIKE :' . $this->getParameterKey(), ); diff --git a/src/Filter/Range.php b/src/Filter/Range.php index 8711c7d2906b45269c34a9cd7a1a385deb475702..ad5977c01189d24812abab70a333ec8a75572503 100644 --- a/src/Filter/Range.php +++ b/src/Filter/Range.php @@ -17,7 +17,7 @@ final class Range extends HttpFilter } if (isset($this->getHttpValue()['min'])) { - $queryBuilder->where( + $queryBuilder->andWhere( $this->getColumn() . ' >= ' . $this->getHttpValue()['min'], ); } diff --git a/src/Filter/Where.php b/src/Filter/Where.php index 8a6a80752918ca24e44add6847e1deddcda56d2f..d7ad73e9a022e3d01546df700a9c0ef6e6df79fc 100644 --- a/src/Filter/Where.php +++ b/src/Filter/Where.php @@ -15,7 +15,7 @@ final class Where extends HttpFilter return $queryBuilder; } - $queryBuilder->where( + $queryBuilder->andWhere( $this->getColumn() . ' = :' . $this->getParameterKey(), ); diff --git a/src/HttpFilterEntityRepository.php b/src/HttpFilterEntityRepository.php index fb0108caa0a3443b095ed45878ce1e5c8f965c70..23d844bbabff6390361d0c7f17e2ba2eb536bb51 100644 --- a/src/HttpFilterEntityRepository.php +++ b/src/HttpFilterEntityRepository.php @@ -29,4 +29,20 @@ class HttpFilterEntityRepository extends EntityRepository implements QueryFilter return $queryBuilder; } + + public function createQueryForSort( + string $field, + string $tableAlias, + Request $request, + ?QueryBuilder $queryBuilder = null + ): QueryBuilder { + if ($queryBuilder === null) { + $queryBuilder = $this->createQueryBuilder($tableAlias); + } + + $sort = new HttpSort($tableAlias, $field, $request); + $sort->addToQuery($queryBuilder); + + return $queryBuilder; + } } diff --git a/src/HttpFilterTrait.php b/src/HttpFilterTrait.php index 373eb5dd683651d3cd9a70bd32d8cfc60bc0ad77..fd1fab724e9685e70fa5ea03df0cdc41c19f94de 100644 --- a/src/HttpFilterTrait.php +++ b/src/HttpFilterTrait.php @@ -28,5 +28,18 @@ trait HttpFilterTrait return $this->repository->createQueryByFilter($filters, $this->getAliasTableForFilter(), $request); } + final public function createQueryForSort(Request $request, ?QueryBuilder $queryBuilder = null): QueryBuilder + { + if (! isset($this->repository)) { + $this->repository = new HttpFilterEntityRepository( + $this->getEntityManager(), + new ClassMetadata($this->getClassName()) + ); + } + $field = array_key_first($request->query->getIterator()[HttpSort::REQUEST_SORT_KEY]); + + return $this->repository->createQueryForSort($field, $this->getAliasTableForFilter(), $request, $queryBuilder); + } + abstract public function getAliasTableForFilter(): string; } diff --git a/src/HttpSort.php b/src/HttpSort.php new file mode 100644 index 0000000000000000000000000000000000000000..a9ebc205caa39d045846dca585c345bf59de24b3 --- /dev/null +++ b/src/HttpSort.php @@ -0,0 +1,54 @@ +request = $request ?? Request::createFromGlobals(); + } + + protected function getColumn(): string + { + return $this->tableAlias . '.' . $this->field; + } + + protected function getHttpValue(): mixed + { + $filter = $this + ->request + ->query + ->getIterator()[static::REQUEST_SORT_KEY] ?? null; + + if ($filter === null) { + return null; + } + + return $filter[$this->field] ?? null; + } + + public function addToQuery(QueryBuilder $queryBuilder): QueryBuilder + { + $orderColumns = str_contains($this->field, '.') ? $this->field : $this->getColumn(); + if (in_array($this->getHttpValue(), self::SORT_DIRECTIONS, true)) { + $queryBuilder->orderBy($orderColumns, $this->getHttpValue()); + } + + return $queryBuilder; + } +} diff --git a/tests/FilterByMultipleFiltersTest.php b/tests/FilterByMultipleFiltersTest.php new file mode 100644 index 0000000000000000000000000000000000000000..7c84bca0fe43435e7abb9f61d9231fc36ecfc87e --- /dev/null +++ b/tests/FilterByMultipleFiltersTest.php @@ -0,0 +1,162 @@ +em->getRepository(Post::class); + + $firstName = 'Видеопредставление'; + $firstDate = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2020-01-01 12:00:00'); + $secondName = 'Видеомагнитофон'; + $secondDate = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2020-01-05 14:00:00'); + + $post = new Post( + $firstName, + $this->faker->text(), + $this->faker->boolean(), + $firstDate, + ); + + $post2 = new Post( + $secondName, + $this->faker->text(), + $this->faker->boolean(), + $secondDate, + ); + + $this->em->persist($post); + $this->em->persist($post2); + $this->em->flush(); + + $subtitle = 'Видео'; + $firstDateWithDay = $firstDate->add(new DateInterval('P1D')); + $result = $postRepository->createQueryByFilter([ + 'createdAt' => DateRange::class, + 'title' => Like::class, + ], new Request([ + HttpFilter::REQUEST_FILTER_KEY => [ + 'title' => $subtitle, + 'createdAt' => [ + 'from' => $firstDate->format('Y-m-d'), + 'to' => $firstDateWithDay->format('Y-m-d'), + ], + ], + ])) + ->getQuery() + ->getResult(); + + $this->assertNotEmpty($result); + $this->assertCount(1, $result); + } + + public function testFilterByMultipleFiltersDateAndIn(): void + { + /** @var PostRepository $postRepository */ + $postRepository = $this->em->getRepository(Post::class); + + $firstName = 'Спорт'; + $firstDate = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2020-01-01 12:00:00'); + $secondName = 'Наука'; + $secondDate = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2020-01-01 14:00:00'); + + $post = new Post( + $firstName, + $this->faker->text(), + $this->faker->boolean(), + $firstDate, + ); + + $post2 = new Post( + $secondName, + $this->faker->text(), + $this->faker->boolean(), + $secondDate, + ); + + $this->em->persist($post); + $this->em->persist($post2); + $this->em->flush(); + + $inValues = ['Спорт', 'Бизнес', 'Кулинария']; + $result = $postRepository->createQueryByFilter([ + 'createdAt' => Date::class, + 'title' => In::class, + ], new Request([ + HttpFilter::REQUEST_FILTER_KEY => [ + 'title' => $inValues, + 'createdAt' => $firstDate->format('Y-m-d'), + ], + ])) + ->getQuery() + ->getResult(); + + $this->assertNotEmpty($result); + $this->assertCount(1, $result); + } + + public function testFilterByMultipleFiltersRangeAndWhere(): void + { + /** @var PostRepository $postRepository */ + $postRepository = $this->em->getRepository(Post::class); + + $firstCountOfViews = 20; + $secondCountOfViews = 40; + + $post = new Post( + $this->faker->name(), + $this->faker->text(), + $this->faker->boolean(), + DateTimeImmutable::createFromInterface($this->faker->dateTime()), + countOfViews: $firstCountOfViews, + ); + + $post2 = new Post( + $this->faker->name(), + $this->faker->text(), + $this->faker->boolean(), + DateTimeImmutable::createFromInterface($this->faker->dateTime()), + countOfViews: $secondCountOfViews, + ); + + $this->em->persist($post); + $this->em->persist($post2); + $this->em->flush(); + + $minValue = 30; + $name = 'Not found'; + $result = $postRepository->createQueryByFilter([ + 'title' => Where::class, + 'countOfViews' => Range::class, + ], new Request([ + HttpFilter::REQUEST_FILTER_KEY => [ + 'title' => $name, + 'createdAt' => [ + 'min' => $minValue, + ] + ], + ])) + ->getQuery() + ->getResult(); + + $this->assertEmpty($result); + } +} diff --git a/tests/HttpSortTest.php b/tests/HttpSortTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d0d4737139f2dea8a0fb6b8e43b528ba7838e7d3 --- /dev/null +++ b/tests/HttpSortTest.php @@ -0,0 +1,225 @@ +em->getRepository(Post::class); + + $title = 'Видеопредставление'; + $title2 = 'Видео'; + $countOfViews = 12; + $countOfViews2 = 22; + + $post = new Post( + $title, + $this->faker->text(), + $this->faker->boolean(), + \DateTimeImmutable::createFromInterface($this->faker->dateTime()), + countOfViews: $countOfViews + ); + + $post2 = new Post( + $title2, + $this->faker->text(), + $this->faker->boolean(), + \DateTimeImmutable::createFromInterface($this->faker->dateTime()), + countOfViews: $countOfViews2 + ); + + $this->em->persist($post); + $this->em->persist($post2); + $this->em->flush(); + + $result = $postRepository->createQueryByFilter([ + 'title' => Like::class, + ], new Request([ + HttpFilter::REQUEST_FILTER_KEY => [ + 'title' => $title2, + ], + ])); + + $result = $postRepository->createQueryForSort( + new Request([ + HttpSort::REQUEST_SORT_KEY => [ + 'countOfViews' => 'DESC', + ], + ]), + $result + ) + ->getQuery() + ->getResult(); + + $this->assertSame($countOfViews, $result[1]->countOfViews); + $this->assertCount(2, $result); + } + + public function testStringSortWithFilter(): void + { + /** @var PostRepository $postRepository */ + $postRepository = $this->em->getRepository(Post::class); + + $title = 'Видеопредставление'; + $title2 = 'Дорога'; + + $post = new Post( + $title, + $this->faker->text(), + $this->faker->boolean(), + \DateTimeImmutable::createFromInterface($this->faker->dateTime()), + ); + + $post2 = new Post( + $title2, + $this->faker->text(), + $this->faker->boolean(), + \DateTimeImmutable::createFromInterface($this->faker->dateTime()), + ); + + $this->em->persist($post); + $this->em->persist($post2); + $this->em->flush(); + + $result = $postRepository->createQueryByFilter([ + 'title' => Like::class, + ], new Request([ + HttpFilter::REQUEST_FILTER_KEY => [ + 'title' => $title2, + ], + ])); + + $result = $postRepository->createQueryForSort( + new Request([ + HttpSort::REQUEST_SORT_KEY => [ + 'title' => 'ASC', + ], + ]), + $result + ) + ->getQuery() + ->getResult(); + + $this->assertSame($title2, $result[0]->title); + $this->assertCount(1, $result); + } + + public function testDateSortWithFilter(): void + { + /** @var PostRepository $postRepository */ + $postRepository = $this->em->getRepository(Post::class); + + $title = 'Видеопредставление'; + $title2 = 'Видео'; + $date = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2020-01-01 12:00:00'); + $date2 = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2020-01-05 19:30:00'); + + $post = new Post( + $title, + $this->faker->text(), + $this->faker->boolean(), + $date + ); + + $post2 = new Post( + $title2, + $this->faker->text(), + $this->faker->boolean(), + $date2 + ); + + $this->em->persist($post); + $this->em->persist($post2); + $this->em->flush(); + + $result = $postRepository->createQueryByFilter([ + 'title' => Like::class, + ], new Request([ + HttpFilter::REQUEST_FILTER_KEY => [ + 'title' => $title2, + ], + ])); + + $result = $postRepository->createQueryForSort( + new Request([ + HttpSort::REQUEST_SORT_KEY => [ + 'createdAt' => 'DESC', + ], + ]), + $result + ) + ->getQuery() + ->getResult(); + + $this->assertSame($date2, $result[0]->createdAt); + $this->assertCount(2, $result); + } + + public function testSortWithJoinTable(): void + { + /** @var PostRepository $postRepository */ + $postRepository = $this->em->getRepository(Post::class); + + $author1 = 'Александр'; + $author2 = 'Яков'; + + $post = new Post( + $this->faker->name(), + $this->faker->text(), + $this->faker->boolean(), + \DateTimeImmutable::createFromInterface($this->faker->dateTime()), + ); + + $comment1 = new Comment( + $author1, + $this->faker->text(), + \DateTimeImmutable::createFromInterface($this->faker->dateTime()), + $post + ); + $comment2 = new Comment( + $author1, + $this->faker->text(), + \DateTimeImmutable::createFromInterface($this->faker->dateTime()), + $post + ); + $comment3 = new Comment( + $author2, + $this->faker->text(), + \DateTimeImmutable::createFromInterface($this->faker->dateTime()), + $post + ); + + $this->em->persist($post); + $this->em->persist($comment1); + $this->em->persist($comment2); + $this->em->persist($comment3); + $this->em->flush(); + + $result = $postRepository->createQueryForSort( + new Request([ + HttpSort::REQUEST_SORT_KEY => [ + 'c.author' => 'DESC', + ], + ]) + ) + ->leftJoin(Comment::class, 'c') + ->select('p', 'c.author') + ->getQuery() + ->getResult(AbstractQuery::HYDRATE_ARRAY); + + $this->assertSame($author2, $result[0]['author']); + } +} diff --git a/tests/Repository/CommentRepository.php b/tests/Repository/CommentRepository.php index c592a7f3213ef5945275e45ad83f232f5651d625..f691197413d8418c6c40c84bfdb85b3331657365 100644 --- a/tests/Repository/CommentRepository.php +++ b/tests/Repository/CommentRepository.php @@ -5,12 +5,15 @@ declare(strict_types=1); namespace IQDEV\Tests\Packages\DoctrineHttpFilter\Repository; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; -use IQDEV\Packages\DoctrineHttpFilter\HttpFilterEntityRepository; +use IQDEV\Packages\DoctrineHttpFilter\HttpFilterTrait; use IQDEV\Tests\Packages\DoctrineHttpFilter\Entity\Comment; -class CommentRepository extends HttpFilterEntityRepository +class CommentRepository extends EntityRepository { + use HttpFilterTrait; + public function __construct(EntityManagerInterface $em) { parent::__construct($em, new ClassMetadata(Comment::class)); diff --git a/tests/Repository/PostRepository.php b/tests/Repository/PostRepository.php index 3e1c3771a6fff6e216f0798dffed22cc29a4aa1b..7cd79106d080040696a9dcb5d44978477bd5a8f6 100644 --- a/tests/Repository/PostRepository.php +++ b/tests/Repository/PostRepository.php @@ -5,12 +5,15 @@ declare(strict_types=1); namespace IQDEV\Tests\Packages\DoctrineHttpFilter\Repository; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; -use IQDEV\Packages\DoctrineHttpFilter\HttpFilterEntityRepository; +use IQDEV\Packages\DoctrineHttpFilter\HttpFilterTrait; use IQDEV\Tests\Packages\DoctrineHttpFilter\Entity\Post; -class PostRepository extends HttpFilterEntityRepository +class PostRepository extends EntityRepository { + use HttpFilterTrait; + public function __construct(EntityManagerInterface $em) { parent::__construct($em, new ClassMetadata(Post::class));