From a7de91ea31d5346cc59d422c8f87b5439a4f2f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0=20=D0=A7=D0=B5=D1=80?= =?UTF-8?q?=D0=BD=D1=8B=D1=85?= Date: Wed, 26 Feb 2025 10:19:50 +0500 Subject: [PATCH] =?UTF-8?q?feature:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B1=D0=B0=D0=B7=D0=B0=20=D1=84=D0=B8?= =?UTF-8?q?=D0=BB=D1=8C=D1=82=D1=80=D0=BE=D0=B2=20=D0=B8=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B2=D1=8B=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .php-cs-fixer.php | 3 +- composer.json | 6 +- src/.gitkeep | 0 src/Filter/Like.php | 20 +++++ src/Filter/Where.php | 22 +++++ src/HttpFilter.php | 49 +++++++++++ src/HttpFilterEntityRepository.php | 35 ++++++++ src/QueryFilterInterface.php | 14 +++ tests/Entity/Comment.php | 46 ++++++++++ tests/Entity/Post.php | 53 ++++++++++++ tests/FilterByWhereTest.php | 113 +++++++++++++++++++++++++ tests/Fixture/BaseFixture.php | 21 +++++ tests/Fixture/CommentFixture.php | 42 +++++++++ tests/Fixture/PostFixture.php | 27 ++++++ tests/Repository/CommentRepository.php | 23 +++++ tests/Repository/PostRepository.php | 23 +++++ tests/TestCase.php | 96 ++++++++++++++++++++- 18 files changed, 591 insertions(+), 3 deletions(-) delete mode 100644 src/.gitkeep create mode 100644 src/Filter/Like.php create mode 100644 src/Filter/Where.php create mode 100644 src/HttpFilter.php create mode 100644 src/HttpFilterEntityRepository.php create mode 100644 src/QueryFilterInterface.php create mode 100644 tests/Entity/Comment.php create mode 100644 tests/Entity/Post.php create mode 100644 tests/FilterByWhereTest.php create mode 100644 tests/Fixture/BaseFixture.php create mode 100644 tests/Fixture/CommentFixture.php create mode 100644 tests/Fixture/PostFixture.php create mode 100644 tests/Repository/CommentRepository.php create mode 100644 tests/Repository/PostRepository.php diff --git a/.gitignore b/.gitignore index d018b8a..6df4f5d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ ### Composer ### composer.phar +composer.lock /vendor/ /var/ diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 42cbf25..4513e21 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -5,7 +5,8 @@ use PhpCsFixer\Finder; $finder = Finder::create() ->in([ - __DIR__ . '/app', + __DIR__ . '/src', + __DIR__ . '/tests', ]) ->name('*.php'); diff --git a/composer.json b/composer.json index 68f0c43..9a44ded 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,11 @@ }, "require-dev": { "phpunit/phpunit": "^12.0", - "friendsofphp/php-cs-fixer": "^3.70" + "friendsofphp/php-cs-fixer": "^3.70", + "symfony/cache": "^7.2", + "doctrine/migrations": "^3.8", + "doctrine/data-fixtures": "^2.0", + "fakerphp/faker": "^1.24" }, "license": "MIT", "autoload": { diff --git a/src/.gitkeep b/src/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Filter/Like.php b/src/Filter/Like.php new file mode 100644 index 0000000..84c93eb --- /dev/null +++ b/src/Filter/Like.php @@ -0,0 +1,20 @@ +where( + $this->getColumn() . ' LIKE \'%' . $this->getHttpValue() . '%\'', + ); + + return $queryBuilder; + } +} diff --git a/src/Filter/Where.php b/src/Filter/Where.php new file mode 100644 index 0000000..cbfc9c7 --- /dev/null +++ b/src/Filter/Where.php @@ -0,0 +1,22 @@ +where( + $this->getColumn() . ' = :' . $this->getParameterKey(), + ); + + $queryBuilder->setParameter($this->getParameterKey(), $this->getHttpValue()); + + return $queryBuilder; + } +} diff --git a/src/HttpFilter.php b/src/HttpFilter.php new file mode 100644 index 0000000..9f16b43 --- /dev/null +++ b/src/HttpFilter.php @@ -0,0 +1,49 @@ +request = $request ?? Request::createFromGlobals(); + } + + protected function getColumn(): string + { + return $this->tableAlias . '.' . $this->filed; + } + + protected function getHttpValue(): mixed + { + $filter = $this + ->request + ->query + ->getIterator()[static::REQUEST_FILTER_KEY] ?? null; + + if ($filter === null) { + return null; + } + + return $filter[$this->filed] ?? null; + } + + public function getParameterKey(): string + { + return str_replace('.', '_', $this->getColumn()); + } + + abstract public function addToQuery(QueryBuilder $queryBuilder): QueryBuilder; +} diff --git a/src/HttpFilterEntityRepository.php b/src/HttpFilterEntityRepository.php new file mode 100644 index 0000000..3e67667 --- /dev/null +++ b/src/HttpFilterEntityRepository.php @@ -0,0 +1,35 @@ +getAliasTableForFilter(); + $queryBuilder = $this->createQueryBuilder($tableAlias); + + /** + * @var string $field + * @var string|HttpFilter $filter + */ + foreach ($filters as $field => $filter) { + if (! $filter instanceof HttpFilter && is_string($filter)) { + $filter = new $filter($tableAlias, $field, $request); + } + + $filter->addToQuery($queryBuilder); + } + + return $queryBuilder; + } + + abstract public function getAliasTableForFilter(): string; +} diff --git a/src/QueryFilterInterface.php b/src/QueryFilterInterface.php new file mode 100644 index 0000000..8522704 --- /dev/null +++ b/src/QueryFilterInterface.php @@ -0,0 +1,14 @@ + $filters */ + public function createQueryByFilter(iterable $filters, ?Request $request = null): QueryBuilder; +} diff --git a/tests/Entity/Comment.php b/tests/Entity/Comment.php new file mode 100644 index 0000000..9211b77 --- /dev/null +++ b/tests/Entity/Comment.php @@ -0,0 +1,46 @@ +author = $author; + $this->content = $content; + $this->createdAt = $createdAt; + $this->updatedAt = $updatedAt; + $this->post = $post; + } +} diff --git a/tests/Entity/Post.php b/tests/Entity/Post.php new file mode 100644 index 0000000..9983d38 --- /dev/null +++ b/tests/Entity/Post.php @@ -0,0 +1,53 @@ + false])] + public ?bool $moderated = null; + + #[ORM\Column(name: 'created_at', type: 'datetime_immutable')] + public ?\DateTimeImmutable $createdAt = null; + + #[ORM\Column(name: 'updated_at', type: 'datetime_immutable', nullable: true)] + public ?\DateTimeImmutable $updatedAt = null; + + #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'post')] + public Collection $comments; + + public function __construct( + string $title, + ?string $content, + ?bool $moderated, + ?\DateTimeImmutable $createdAt, + ?\DateTimeImmutable $updatedAt = null, + ) { + $this->title = $title; + $this->content = $content; + $this->moderated = $moderated; + $this->createdAt = $createdAt; + $this->updatedAt = $updatedAt; + + $this->comments = new ArrayCollection(); + } +} diff --git a/tests/FilterByWhereTest.php b/tests/FilterByWhereTest.php new file mode 100644 index 0000000..a056b47 --- /dev/null +++ b/tests/FilterByWhereTest.php @@ -0,0 +1,113 @@ +em->getRepository(Post::class); + + $title = $this->faker->name(); + + $post = new Post( + $title, + $this->faker->text(), + $this->faker->boolean(), + \DateTimeImmutable::createFromInterface($this->faker->dateTime()), + ); + + $post2 = new Post( + $this->faker->name(), + $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' => Where::class, + ], new Request([ + HttpFilter::REQUEST_FILTER_KEY => [ + 'title' => $title, + ], + ])) + ->getQuery() + ->getResult(); + + $this->assertNotEmpty($result); + $this->assertCount(1, $result); + $this->assertEquals($title, current($result)->title); + } + + public function testSuccessFilterWhereWithSeveralResult(): void + { + /** @var PostRepository $postRepository */ + $postRepository = $this->em->getRepository(Post::class); + + $title = $this->faker->name(); + + $post = new Post( + $title, + $this->faker->text(), + $this->faker->boolean(), + \DateTimeImmutable::createFromInterface($this->faker->dateTime()), + ); + + $post2 = new Post( + $title, + $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' => Where::class, + ], new Request([ + HttpFilter::REQUEST_FILTER_KEY => [ + 'title' => $title, + ], + ])) + ->getQuery() + ->getResult(); + + $this->assertNotEmpty($result); + $this->assertCount(2, $result); + $this->assertEquals($title, current($result)->title); + $this->assertEquals($title, next($result)->title); + } + + public function testFilterWhereWithNotResult(): void + { + /** @var PostRepository $postRepository */ + $postRepository = $this->em->getRepository(Post::class); + + $result = $postRepository->createQueryByFilter([ + 'title' => Where::class, + ], new Request([ + HttpFilter::REQUEST_FILTER_KEY => [ + 'title' => 'Стимул', + ], + ])) + ->getQuery() + ->getResult(); + + $this->assertEmpty($result); + } +} diff --git a/tests/Fixture/BaseFixture.php b/tests/Fixture/BaseFixture.php new file mode 100644 index 0000000..8a5f9bf --- /dev/null +++ b/tests/Fixture/BaseFixture.php @@ -0,0 +1,21 @@ +faker = Factory::create($locale); + } +} diff --git a/tests/Fixture/CommentFixture.php b/tests/Fixture/CommentFixture.php new file mode 100644 index 0000000..ee45770 --- /dev/null +++ b/tests/Fixture/CommentFixture.php @@ -0,0 +1,42 @@ +getRepository(Post::class); + + /** @var array $posts */ + $posts = $postRepository->findAll(); + + for ($i = 0; $i < $this->count; $i++) { + $comment = new Comment( + $this->faker->userName(), + $this->faker->text(), + \DateTimeImmutable::createFromInterface($this->faker->dateTime()), + $this->faker->randomElement($posts), + ); + + $manager->persist($comment); + } + + $manager->flush(); + } + + public function getDependencies(): array + { + return [ + PostFixture::class, + ]; + } +} diff --git a/tests/Fixture/PostFixture.php b/tests/Fixture/PostFixture.php new file mode 100644 index 0000000..8a2e1f9 --- /dev/null +++ b/tests/Fixture/PostFixture.php @@ -0,0 +1,27 @@ +count; ++$i) { + $post = new Post( + $this->faker->name(), + $this->faker->text(), + $this->faker->boolean(), + \DateTimeImmutable::createFromInterface($this->faker->dateTime()), + ); + + $manager->persist($post); + } + + $manager->flush(); + } +} diff --git a/tests/Repository/CommentRepository.php b/tests/Repository/CommentRepository.php new file mode 100644 index 0000000..c592a7f --- /dev/null +++ b/tests/Repository/CommentRepository.php @@ -0,0 +1,23 @@ +em = $this->makeEntityManager(); + + $schemaTool = new SchemaTool($this->em); + $schemaTool->createSchema($this->em->getMetadataFactory()->getAllMetadata()); + + $this->faker = Factory::create(); + + $this->loadFixtures([ + new PostFixture(count: 20), + new CommentFixture(200), + ]); + + $this->em->getConnection()->beginTransaction(); + } + + protected function tearDown(): void + { + + if ($this->em->isOpen()) { + $this->em->getConnection()->rollBack(); + $this->em->close(); + } + unset($this->em); + } + + protected function makeEntityManager(): EntityManagerInterface + { + $config = ORMSetup::createAttributeMetadataConfiguration( + [__DIR__ . '/Entity'], + true, + ); + + $connection = DriverManager::getConnection([ + 'driver' => 'pdo_sqlite', + 'memory' => true, + ], $config); + + return new EntityManager( + $connection, + $config, + ); + } + + protected function makeRepositoryWithRequest(string $repositoryClass, Request $request): HttpFilterEntityRepository + { + /** @var HttpFilterEntityRepository $repoMock */ + $repoMock = $this->getMockBuilder($repositoryClass) + ->setConstructorArgs([$this->em]) + ->onlyMethods(['getRequest']) + ->getMock(); + + $repoMock + ->method('getRequest') + ->willReturn($request); + + return $repoMock; + } + + /** @param iterable $fixtures */ + protected function loadFixtures(iterable $fixtures): void + { + $fixtureLoader = new Loader(); + + /** @var BaseFixture $fixture */ + foreach ($fixtures as $fixture) { + $fixtureLoader->addFixture($fixture); + } + + $purger = new ORMPurger(); + $executor = new ORMExecutor($this->em, $purger); + $executor->execute($fixtureLoader->getFixtures()); + } +} -- GitLab