mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-25 06:11:11 +03:00
Added happyr/doctrine-specification to support dunamically applying specs to queries
This commit is contained in:
parent
90551ff3bc
commit
ecf22ae4b6
11 changed files with 48 additions and 28 deletions
|
@ -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",
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue