Added happyr/doctrine-specification to support dunamically applying specs to queries

This commit is contained in:
Alejandro Celaya 2021-01-02 17:14:42 +01:00
parent 90551ff3bc
commit ecf22ae4b6
11 changed files with 48 additions and 28 deletions

View file

@ -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",

View file

@ -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 = <<<DQL
SELECT t AS tag, COUNT(DISTINCT s.id) AS shortUrlsCount, COUNT(DISTINCT v.id) AS visitsCount
FROM Shlinkio\Shlink\Core\Entity\Tag t
LEFT JOIN t.shortUrls s
LEFT JOIN s.visits v
GROUP BY t
ORDER BY t.name ASC
DQL;
$query = $this->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(),

View file

@ -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;
}

View file

@ -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);
}
/**

View file

@ -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

View file

@ -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();

View file

@ -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([

View file

@ -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();
}
}

View file

@ -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([

View file

@ -16,6 +16,9 @@
<directory suffix=".php">./module/*/src/Repository</directory>
<directory suffix=".php">./module/*/src/**/Repository</directory>
<directory suffix=".php">./module/*/src/**/**/Repository</directory>
<directory suffix=".php">./module/*/src/Spec</directory>
<directory suffix=".php">./module/*/src/**/Spec</directory>
<directory suffix=".php">./module/*/src/**/**/Spec</directory>
</include>
</coverage>
</phpunit>

View file

@ -25,6 +25,9 @@
<directory suffix=".php">./module/Core/src/Repository</directory>
<directory suffix=".php">./module/Core/src/**/Repository</directory>
<directory suffix=".php">./module/Core/src/**/**/Repository</directory>
<directory suffix=".php">./module/Core/src/Spec</directory>
<directory suffix=".php">./module/Core/src/**/Spec</directory>
<directory suffix=".php">./module/Core/src/**/**/Spec</directory>
</exclude>
</coverage>
</phpunit>