diff --git a/.gitignore b/.gitignore index d018b8aae30880fd0050f42f304e769741071f1b..6df4f5d472268ead46f3bb694e7d7e2e90841091 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 42cbf259db89513b288ea23449186c2fd83a0397..4513e2166a3b2a7e09c31b1b80fc41b613dd3b0b 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 68f0c43a6dc892a58e35dbd169aad20bab5cd1cb..9a44ded75a1a9062008a1e9a001f9be90fa1396e 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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/Filter/Like.php b/src/Filter/Like.php new file mode 100644 index 0000000000000000000000000000000000000000..84c93eb258e9525c73ea706d5aa5573517d37730 --- /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 0000000000000000000000000000000000000000..cbfc9c7f3c80efcbe9e18cdd3a9f9b7efba9903d --- /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 0000000000000000000000000000000000000000..9f16b43d6841495b17a8593ed8124338cd74d95b --- /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 0000000000000000000000000000000000000000..3e67667298274192ff4ee0e3bb5b8fff744164cf --- /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 0000000000000000000000000000000000000000..8522704c32d26617516050bd5f15b6a18bf645d1 --- /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 0000000000000000000000000000000000000000..9211b779ae12a19203a34c388811f518e03aec9d --- /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 0000000000000000000000000000000000000000..9983d381a9fbaa3ce25274ea341a7547d526af6c --- /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 0000000000000000000000000000000000000000..a056b47ac5cfb10b460e0cb2975b814ebfbec637 --- /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 0000000000000000000000000000000000000000..8a5f9bf56bc734e5ba4e5be5560bb26cfab8a887 --- /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 0000000000000000000000000000000000000000..ee45770f2ee2c6d8e8707f4275a859bf2665fc03 --- /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 0000000000000000000000000000000000000000..8a2e1f94931eb4dc3a4bb51ef6e9c49ec7f79dc5 --- /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 0000000000000000000000000000000000000000..c592a7f3213ef5945275e45ad83f232f5651d625 --- /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()); + } +}