mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-29 01:18:59 +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",
|
"endroid/qr-code": "^3.6",
|
||||||
"geoip2/geoip2": "^2.9",
|
"geoip2/geoip2": "^2.9",
|
||||||
"guzzlehttp/guzzle": "^7.0",
|
"guzzlehttp/guzzle": "^7.0",
|
||||||
|
"happyr/doctrine-specification": "2.0.x-dev as 2.0",
|
||||||
"laminas/laminas-config": "^3.3",
|
"laminas/laminas-config": "^3.3",
|
||||||
"laminas/laminas-config-aggregator": "^1.1",
|
"laminas/laminas-config-aggregator": "^1.1",
|
||||||
"laminas/laminas-diactoros": "^2.1.3",
|
"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": "@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:unit:ci": "@test:unit --coverage-xml=build/coverage-unit/coverage-xml --log-junit=build/coverage-unit/junit.xml",
|
||||||
"test:db": [
|
"test:db": "@parallel test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms",
|
||||||
"@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": "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: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",
|
"test:db:mysql": "DB_DRIVER=mysql composer test:db:sqlite",
|
||||||
|
|
|
@ -4,13 +4,14 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\Core\Repository;
|
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\Entity\Tag;
|
||||||
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
||||||
|
|
||||||
use function Functional\map;
|
use function Functional\map;
|
||||||
|
|
||||||
class TagRepository extends EntityRepository implements TagRepositoryInterface
|
class TagRepository extends EntitySpecificationRepository implements TagRepositoryInterface
|
||||||
{
|
{
|
||||||
public function deleteByName(array $names): int
|
public function deleteByName(array $names): int
|
||||||
{
|
{
|
||||||
|
@ -28,17 +29,16 @@ class TagRepository extends EntityRepository implements TagRepositoryInterface
|
||||||
/**
|
/**
|
||||||
* @return TagInfo[]
|
* @return TagInfo[]
|
||||||
*/
|
*/
|
||||||
public function findTagsWithInfo(): array
|
public function findTagsWithInfo(?Specification $spec = null): array
|
||||||
{
|
{
|
||||||
$dql = <<<DQL
|
$qb = $this->getQueryBuilder($spec, 't');
|
||||||
SELECT t AS tag, COUNT(DISTINCT s.id) AS shortUrlsCount, COUNT(DISTINCT v.id) AS visitsCount
|
$qb->select('t AS tag', 'COUNT(DISTINCT s.id) AS shortUrlsCount', 'COUNT(DISTINCT v.id) AS visitsCount')
|
||||||
FROM Shlinkio\Shlink\Core\Entity\Tag t
|
->leftJoin('t.shortUrls', 's')
|
||||||
LEFT JOIN t.shortUrls s
|
->leftJoin('s.visits', 'v')
|
||||||
LEFT JOIN s.visits v
|
->groupBy('t')
|
||||||
GROUP BY t
|
->orderBy('t.name', 'ASC');
|
||||||
ORDER BY t.name ASC
|
|
||||||
DQL;
|
$query = $qb->getQuery();
|
||||||
$query = $this->getEntityManager()->createQuery($dql);
|
|
||||||
|
|
||||||
return map(
|
return map(
|
||||||
$query->getResult(),
|
$query->getResult(),
|
||||||
|
|
|
@ -5,14 +5,16 @@ declare(strict_types=1);
|
||||||
namespace Shlinkio\Shlink\Core\Repository;
|
namespace Shlinkio\Shlink\Core\Repository;
|
||||||
|
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
use Happyr\DoctrineSpecification\EntitySpecificationRepositoryInterface;
|
||||||
|
use Happyr\DoctrineSpecification\Specification\Specification;
|
||||||
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
||||||
|
|
||||||
interface TagRepositoryInterface extends ObjectRepository
|
interface TagRepositoryInterface extends ObjectRepository, EntitySpecificationRepositoryInterface
|
||||||
{
|
{
|
||||||
public function deleteByName(array $names): int;
|
public function deleteByName(array $names): int;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return TagInfo[]
|
* @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\Repository\TagRepositoryInterface;
|
||||||
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
||||||
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
|
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
|
||||||
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
|
||||||
class TagService implements TagServiceInterface
|
class TagService implements TagServiceInterface
|
||||||
{
|
{
|
||||||
|
@ -38,11 +39,11 @@ class TagService implements TagServiceInterface
|
||||||
/**
|
/**
|
||||||
* @return TagInfo[]
|
* @return TagInfo[]
|
||||||
*/
|
*/
|
||||||
public function tagsInfo(): array
|
public function tagsInfo(?ApiKey $apiKey = null): array
|
||||||
{
|
{
|
||||||
/** @var TagRepositoryInterface $repo */
|
/** @var TagRepositoryInterface $repo */
|
||||||
$repo = $this->em->getRepository(Tag::class);
|
$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\TagConflictException;
|
||||||
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
|
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
|
||||||
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
||||||
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
|
||||||
interface TagServiceInterface
|
interface TagServiceInterface
|
||||||
{
|
{
|
||||||
|
@ -20,7 +21,7 @@ interface TagServiceInterface
|
||||||
/**
|
/**
|
||||||
* @return TagInfo[]
|
* @return TagInfo[]
|
||||||
*/
|
*/
|
||||||
public function tagsInfo(): array;
|
public function tagsInfo(?ApiKey $apiKey = null): array;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string[] $tagNames
|
* @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)];
|
$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();
|
$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\Model\TagInfo;
|
||||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||||
|
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
|
||||||
|
|
||||||
use function Functional\map;
|
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());
|
$data = map($tagsInfo, fn (TagInfo $info) => (string) $info->tag());
|
||||||
|
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
|
|
|
@ -5,6 +5,8 @@ declare(strict_types=1);
|
||||||
namespace Shlinkio\Shlink\Rest\Entity;
|
namespace Shlinkio\Shlink\Rest\Entity;
|
||||||
|
|
||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
|
use Happyr\DoctrineSpecification\Spec;
|
||||||
|
use Happyr\DoctrineSpecification\Specification\Specification;
|
||||||
use Ramsey\Uuid\Uuid;
|
use Ramsey\Uuid\Uuid;
|
||||||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||||
|
|
||||||
|
@ -59,4 +61,11 @@ class ApiKey extends AbstractEntity
|
||||||
{
|
{
|
||||||
return $this->key;
|
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\Model\TagInfo;
|
||||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||||
use Shlinkio\Shlink\Rest\Action\Tag\ListTagsAction;
|
use Shlinkio\Shlink\Rest\Action\Tag\ListTagsAction;
|
||||||
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
|
||||||
class ListTagsActionTest extends TestCase
|
class ListTagsActionTest extends TestCase
|
||||||
{
|
{
|
||||||
|
@ -62,10 +63,13 @@ class ListTagsActionTest extends TestCase
|
||||||
new TagInfo(new Tag('foo'), 1, 1),
|
new TagInfo(new Tag('foo'), 1, 1),
|
||||||
new TagInfo(new Tag('bar'), 3, 10),
|
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 */
|
/** @var JsonResponse $resp */
|
||||||
$resp = $this->action->handle(ServerRequestFactory::fromGlobals()->withQueryParams(['withStats' => 'true']));
|
$resp = $this->action->handle($req);
|
||||||
$payload = $resp->getPayload();
|
$payload = $resp->getPayload();
|
||||||
|
|
||||||
self::assertEquals([
|
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/**/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>
|
</include>
|
||||||
</coverage>
|
</coverage>
|
||||||
</phpunit>
|
</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/**/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>
|
</exclude>
|
||||||
</coverage>
|
</coverage>
|
||||||
</phpunit>
|
</phpunit>
|
||||||
|
|
Loading…
Reference in a new issue