From ecf22ae4b6dc3e9606561d646840b51f8064d1f5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 2 Jan 2021 17:14:42 +0100 Subject: [PATCH] Added happyr/doctrine-specification to support dunamically applying specs to queries --- composer.json | 9 ++----- module/Core/src/Repository/TagRepository.php | 24 +++++++++---------- .../src/Repository/TagRepositoryInterface.php | 6 +++-- module/Core/src/Tag/TagService.php | 5 ++-- module/Core/src/Tag/TagServiceInterface.php | 3 ++- .../Core/test/Service/Tag/TagServiceTest.php | 2 +- module/Rest/src/Action/Tag/ListTagsAction.php | 4 +++- module/Rest/src/Entity/ApiKey.php | 9 +++++++ .../test/Action/Tag/ListTagsActionTest.php | 8 +++++-- phpunit-db.xml | 3 +++ phpunit.xml.dist | 3 +++ 11 files changed, 48 insertions(+), 28 deletions(-) diff --git a/composer.json b/composer.json index b2828ef9..e0333d85 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "endroid/qr-code": "^3.6", "geoip2/geoip2": "^2.9", "guzzlehttp/guzzle": "^7.0", + "happyr/doctrine-specification": "2.0.x-dev as 2.0", "laminas/laminas-config": "^3.3", "laminas/laminas-config-aggregator": "^1.1", "laminas/laminas-diactoros": "^2.1.3", @@ -125,13 +126,7 @@ ], "test:unit": "@php vendor/bin/phpunit --order-by=random --colors=always --coverage-php build/coverage-unit.cov --testdox", "test:unit:ci": "@test:unit --coverage-xml=build/coverage-unit/coverage-xml --log-junit=build/coverage-unit/junit.xml", - "test:db": [ - "@test:db:sqlite:ci", - "@test:db:mysql", - "@test:db:maria", - "@test:db:postgres", - "@test:db:ms" - ], + "test:db": "@parallel test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms", "test:db:sqlite": "APP_ENV=test php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-db.xml", "test:db:sqlite:ci": "@test:db:sqlite --coverage-php build/coverage-db.cov --coverage-xml=build/coverage-db/coverage-xml --log-junit=build/coverage-db/junit.xml", "test:db:mysql": "DB_DRIVER=mysql composer test:db:sqlite", diff --git a/module/Core/src/Repository/TagRepository.php b/module/Core/src/Repository/TagRepository.php index 05b2481c..3b3c5beb 100644 --- a/module/Core/src/Repository/TagRepository.php +++ b/module/Core/src/Repository/TagRepository.php @@ -4,13 +4,14 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Repository; -use Doctrine\ORM\EntityRepository; +use Happyr\DoctrineSpecification\EntitySpecificationRepository; +use Happyr\DoctrineSpecification\Specification\Specification; use Shlinkio\Shlink\Core\Entity\Tag; use Shlinkio\Shlink\Core\Tag\Model\TagInfo; use function Functional\map; -class TagRepository extends EntityRepository implements TagRepositoryInterface +class TagRepository extends EntitySpecificationRepository implements TagRepositoryInterface { public function deleteByName(array $names): int { @@ -28,17 +29,16 @@ class TagRepository extends EntityRepository implements TagRepositoryInterface /** * @return TagInfo[] */ - public function findTagsWithInfo(): array + public function findTagsWithInfo(?Specification $spec = null): array { - $dql = <<getEntityManager()->createQuery($dql); + $qb = $this->getQueryBuilder($spec, 't'); + $qb->select('t AS tag', 'COUNT(DISTINCT s.id) AS shortUrlsCount', 'COUNT(DISTINCT v.id) AS visitsCount') + ->leftJoin('t.shortUrls', 's') + ->leftJoin('s.visits', 'v') + ->groupBy('t') + ->orderBy('t.name', 'ASC'); + + $query = $qb->getQuery(); return map( $query->getResult(), diff --git a/module/Core/src/Repository/TagRepositoryInterface.php b/module/Core/src/Repository/TagRepositoryInterface.php index 37179e21..a486ef55 100644 --- a/module/Core/src/Repository/TagRepositoryInterface.php +++ b/module/Core/src/Repository/TagRepositoryInterface.php @@ -5,14 +5,16 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Repository; use Doctrine\Persistence\ObjectRepository; +use Happyr\DoctrineSpecification\EntitySpecificationRepositoryInterface; +use Happyr\DoctrineSpecification\Specification\Specification; use Shlinkio\Shlink\Core\Tag\Model\TagInfo; -interface TagRepositoryInterface extends ObjectRepository +interface TagRepositoryInterface extends ObjectRepository, EntitySpecificationRepositoryInterface { public function deleteByName(array $names): int; /** * @return TagInfo[] */ - public function findTagsWithInfo(): array; + public function findTagsWithInfo(?Specification $spec = null): array; } diff --git a/module/Core/src/Tag/TagService.php b/module/Core/src/Tag/TagService.php index 4e0261a5..786e102d 100644 --- a/module/Core/src/Tag/TagService.php +++ b/module/Core/src/Tag/TagService.php @@ -13,6 +13,7 @@ use Shlinkio\Shlink\Core\Repository\TagRepository; use Shlinkio\Shlink\Core\Repository\TagRepositoryInterface; use Shlinkio\Shlink\Core\Tag\Model\TagInfo; use Shlinkio\Shlink\Core\Util\TagManagerTrait; +use Shlinkio\Shlink\Rest\Entity\ApiKey; class TagService implements TagServiceInterface { @@ -38,11 +39,11 @@ class TagService implements TagServiceInterface /** * @return TagInfo[] */ - public function tagsInfo(): array + public function tagsInfo(?ApiKey $apiKey = null): array { /** @var TagRepositoryInterface $repo */ $repo = $this->em->getRepository(Tag::class); - return $repo->findTagsWithInfo(); + return $repo->findTagsWithInfo($apiKey !== null ? $apiKey->spec() : null); } /** diff --git a/module/Core/src/Tag/TagServiceInterface.php b/module/Core/src/Tag/TagServiceInterface.php index 3c8c6e69..bd96a225 100644 --- a/module/Core/src/Tag/TagServiceInterface.php +++ b/module/Core/src/Tag/TagServiceInterface.php @@ -9,6 +9,7 @@ use Shlinkio\Shlink\Core\Entity\Tag; use Shlinkio\Shlink\Core\Exception\TagConflictException; use Shlinkio\Shlink\Core\Exception\TagNotFoundException; use Shlinkio\Shlink\Core\Tag\Model\TagInfo; +use Shlinkio\Shlink\Rest\Entity\ApiKey; interface TagServiceInterface { @@ -20,7 +21,7 @@ interface TagServiceInterface /** * @return TagInfo[] */ - public function tagsInfo(): array; + public function tagsInfo(?ApiKey $apiKey = null): array; /** * @param string[] $tagNames diff --git a/module/Core/test/Service/Tag/TagServiceTest.php b/module/Core/test/Service/Tag/TagServiceTest.php index 16fd8683..c4203c85 100644 --- a/module/Core/test/Service/Tag/TagServiceTest.php +++ b/module/Core/test/Service/Tag/TagServiceTest.php @@ -51,7 +51,7 @@ class TagServiceTest extends TestCase { $expected = [new TagInfo(new Tag('foo'), 1, 1), new TagInfo(new Tag('bar'), 3, 10)]; - $find = $this->repo->findTagsWithInfo()->willReturn($expected); + $find = $this->repo->findTagsWithInfo(null)->willReturn($expected); $result = $this->service->tagsInfo(); diff --git a/module/Rest/src/Action/Tag/ListTagsAction.php b/module/Rest/src/Action/Tag/ListTagsAction.php index 0832f17c..64ddad33 100644 --- a/module/Rest/src/Action/Tag/ListTagsAction.php +++ b/module/Rest/src/Action/Tag/ListTagsAction.php @@ -10,6 +10,7 @@ use Psr\Http\Message\ServerRequestInterface; use Shlinkio\Shlink\Core\Tag\Model\TagInfo; use Shlinkio\Shlink\Core\Tag\TagServiceInterface; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; +use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware; use function Functional\map; @@ -38,7 +39,8 @@ class ListTagsAction extends AbstractRestAction ]); } - $tagsInfo = $this->tagService->tagsInfo(); + $apiKey = AuthenticationMiddleware::apiKeyFromRequest($request); + $tagsInfo = $this->tagService->tagsInfo($apiKey); $data = map($tagsInfo, fn (TagInfo $info) => (string) $info->tag()); return new JsonResponse([ diff --git a/module/Rest/src/Entity/ApiKey.php b/module/Rest/src/Entity/ApiKey.php index a800d530..210c5b3a 100644 --- a/module/Rest/src/Entity/ApiKey.php +++ b/module/Rest/src/Entity/ApiKey.php @@ -5,6 +5,8 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Rest\Entity; use Cake\Chronos\Chronos; +use Happyr\DoctrineSpecification\Spec; +use Happyr\DoctrineSpecification\Specification\Specification; use Ramsey\Uuid\Uuid; use Shlinkio\Shlink\Common\Entity\AbstractEntity; @@ -59,4 +61,11 @@ class ApiKey extends AbstractEntity { return $this->key; } + + /** + */ + public function spec(): Specification + { + return Spec::andX(); + } } diff --git a/module/Rest/test/Action/Tag/ListTagsActionTest.php b/module/Rest/test/Action/Tag/ListTagsActionTest.php index 2f675536..c8b65a48 100644 --- a/module/Rest/test/Action/Tag/ListTagsActionTest.php +++ b/module/Rest/test/Action/Tag/ListTagsActionTest.php @@ -13,6 +13,7 @@ use Shlinkio\Shlink\Core\Entity\Tag; use Shlinkio\Shlink\Core\Tag\Model\TagInfo; use Shlinkio\Shlink\Core\Tag\TagServiceInterface; use Shlinkio\Shlink\Rest\Action\Tag\ListTagsAction; +use Shlinkio\Shlink\Rest\Entity\ApiKey; class ListTagsActionTest extends TestCase { @@ -62,10 +63,13 @@ class ListTagsActionTest extends TestCase new TagInfo(new Tag('foo'), 1, 1), new TagInfo(new Tag('bar'), 3, 10), ]; - $tagsInfo = $this->tagService->tagsInfo()->willReturn($stats); + $apiKey = new ApiKey(); + $tagsInfo = $this->tagService->tagsInfo($apiKey)->willReturn($stats); + $req = ServerRequestFactory::fromGlobals()->withQueryParams(['withStats' => 'true']) + ->withAttribute(ApiKey::class, $apiKey); /** @var JsonResponse $resp */ - $resp = $this->action->handle(ServerRequestFactory::fromGlobals()->withQueryParams(['withStats' => 'true'])); + $resp = $this->action->handle($req); $payload = $resp->getPayload(); self::assertEquals([ diff --git a/phpunit-db.xml b/phpunit-db.xml index a995448f..030f777b 100644 --- a/phpunit-db.xml +++ b/phpunit-db.xml @@ -16,6 +16,9 @@ ./module/*/src/Repository ./module/*/src/**/Repository ./module/*/src/**/**/Repository + ./module/*/src/Spec + ./module/*/src/**/Spec + ./module/*/src/**/**/Spec diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 68f5263a..9c8e02df 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -25,6 +25,9 @@ ./module/Core/src/Repository ./module/Core/src/**/Repository ./module/Core/src/**/**/Repository + ./module/Core/src/Spec + ./module/Core/src/**/Spec + ./module/Core/src/**/**/Spec